线程安全设置
Thread-safe Settings
我正在编写一些可以从我的多线程应用程序中任何地方访问的设置类。我会经常阅读这些设置(因此读取访问应该很快),但它们并不经常写入。
对于原始数据类型,看起来boost::atomic
提供了我需要的东西,所以我想出了这样的东西:
class UInt16Setting
{
private:
boost::atomic<uint16_t> _Value;
public:
uint16_t getValue() const { return _Value.load(boost::memory_order_relaxed); }
void setValue(uint16_t value) { _Value.store(value, boost::memory_order_relaxed); }
};
问题 1:我不确定内存排序。我认为在我的应用程序中,我并不真正关心内存排序(是吗?我只想确保getValue()
始终返回一个未损坏的值(旧值或新值)。那么我的内存排序设置是否正确?
问题 2:是否建议将此方法使用boost::atomic
用于此类同步?或者是否有其他结构可以提供更好的读取性能?
我的应用程序中还需要一些更复杂的设置类型,例如std::string
或boost::asio::ip::tcp::endpoint
列表。我认为所有这些设置值都是不可变的。因此,一旦我使用 setValue()
设置值,值本身(std::string
或端点列表本身)就不再更改。因此,我只想确保我得到旧值或新值,而不是一些损坏的状态。
问题3:这种方法适用于boost::atomic<std::string>
吗?如果没有,还有什么替代方案?
问题 4:更复杂的设置类型(如端点列表)怎么样? 你会推荐像boost::atomic<boost::shared_ptr<std::vector<boost::asio::ip::tcp::endpoint>>>
这样的东西吗?如果没有,还有什么更好的呢?
Q1,如果您在读取原子后不尝试读取任何共享的非原子变量,请更正。内存屏障仅同步对原子操作之间可能发生的非原子变量的访问
Q2 我不知道(但见下文)
Q3 应该可以工作(如果编译)。然而
atomic<string>
可能没有锁定
Q4 应该可以工作,但同样,实现不可能是无锁的(实现无锁shared_ptr具有挑战性且需要专利挖掘)。
因此,如果您的配置包含大小超过 1 个机器字的数据(CPU 本机原子通常适用于此),则读者-写入器锁定(正如 Damon 在评论中建议的那样)可能会更简单,甚至更有效
[编辑]但是,
atomic<shared_ptr<TheWholeStructContainigAll> >
即使无锁也可能有一定的意义:这种方法最大限度地减少了需要多个相干值的读取器的冲突概率,尽管编写器应该在每次更改某些内容时制作整个"参数表"的新副本。
对于问题 1,答案是"取决于,但可能不是"。如果你真的只关心单个值没有乱码,那么是的,这很好,你也不关心内存顺序。
不过,通常情况下,这是一个错误的前提。
对于问题 2、3 和 4,是的,这将起作用,但它可能会对复杂对象(如 string
)使用锁定(在内部,对于每次访问,在您不知情的情况下)。通常只有大约一个或两个指针大小的相当小的对象才能以无锁方式原子方式访问/更改。这也取决于您的平台。
一个人是否以原子方式成功更新一个或两个值是一个很大的区别。假设您有值 left
和 right
,它们分隔了任务将在数组中执行某些处理的位置的左右边界。假设它们分别为 50 和 100,然后将它们分别更改为 101 和 150,每个原子。因此,另一个线程拾取从 50 到 101 的变化并开始计算,看到 101> 100,完成并将结果写入文件。之后,再次以原子方式更改输出文件的名称。
一切都是原子的(因此比正常情况贵),但没有一个是有用的。结果仍然是错误的,并且也被写入了错误的文件。
在您的特定情况下,这可能不是问题,但通常是(并且,您的要求将来可能会发生变化)。通常,您真的希望完整的更改集是原子的。
或者,许多和复杂的)更新要做,你可能首先希望对整个配置使用一个大的(读取器-写入器)锁,因为这比获取和释放 20 或 30 个锁或执行 50 或 100 个原子操作更有效。但请注意,在任何情况下,锁定都会严重影响性能。
正如上面的评论中所指出的,我最好从修改配置的一个线程中制作配置的深层副本,并将消费者使用的引用(共享指针)的更新作为正常任务。这种复制-修改-发布方法也与 MVCC 数据库的工作方式有点相似(这些数据库也存在锁定会降低其性能的问题)。
修改副本断言只有读取器访问任何共享状态,因此读取器或单个写入器都不需要同步。阅读和写作很快。交换配置集仅在保证配置集处于完整、一致的状态并且线程保证不执行其他操作时明确定义的点进行,因此不会发生任何类型的丑陋意外。
典型的任务驱动应用程序看起来有点像这样(在类似C++伪代码中):
// consumer/worker thread(s)
for(;;)
{
task = queue.pop();
switch(task.code)
{
case EXIT:
return;
case SET_CONFIG:
my_conf = task.data;
break;
default:
task.func(task.data, &my_conf); // can read without sync
}
}
// thread that interacts with user (also producer)
for(;;)
{
input = get_input();
if(input.action == QUIT)
{
queue.push(task(EXIT, 0, 0));
for(auto threads : thread)
thread.join();
return 0;
}
else if(input.action == CHANGE_SETTINGS)
{
new_config = new config(config); // copy, readonly operation, no sync
// assume we have operator[] overloaded
new_config[...] = ...; // I own this exclusively, no sync
task t(SET_CONFIG, 0, shared_ptr<...>(input.data));
queue.push(t);
}
else if(input.action() == ADD_TASK)
{
task t(RUN, input.func, input.data);
queue.push(t);
}
...
}
对于比指针更重要的任何内容,请使用互斥锁。tbb(开源)库支持读取器-写入器突变的概念,它允许多个同时读取器,请参阅文档。
- 为不同配置设置MSVC_RUNTIME_LIBRARY的正确方法是什么
- 从不同线程使用int64的不同字节安全吗
- 在C++/Linux中设置单调时钟的一些技巧
- 将数组作为参数传递给函数安全吗?作为第三方职能部门,可以探索他们想要的之外的其他元素
- 如何在选项卡视图Qt中设置一个新项目,并保存以前的项目
- 嵌套在类中时无法设置成员数据
- 虚拟决赛作为安全
- 将常量引用成员设置为临时变量是否安全
- 是否将常量字符*设置为等于字符[]文字安全
- 系统( "bcdedit /set safeboot" ) 未将窗口设置为安全启动
- 一个线程设置成员,而另一个循环上方 - 是此螺纹 - 不安全
- 在std ::设置中存储一个指针的指针是安全的吗?
- C#:如何设置不安全结构的成员阵列
- 更改Internet Explorer安全设置(initaize和Script ActiveX Control in n
- 免疫表中副本的设置是否安全,避免线程损坏
- 将子结构值设置为对象构造函数中安全的纯虚拟函数返回的值
- 使用UTC来回转换日期以忽略DST但仍使用当前用户的有效区域设置是否安全
- 线程安全设置
- 需要为Solaris 10上的GNU g++2.95.3设置线程安全的std::string
- 为什么仅仅边界描述符的安全设置是不够的