为什么std :: atomic initialisation没有原子释放,所以其他线程可以看到初始值?

Nia*_*las 17 c++ multithreading stl atomic c++11

提议的boost :: concurrent_unordered_map的线程清理过程中出现了一些非常奇怪的东西,并在这篇博客文章中进行了叙述.简而言之,bucket_type如下所示:

  struct bucket_type_impl
  {
    spinlock<unsigned char> lock;  // = 2 if you need to reload the bucket list
    atomic<unsigned> count; // count is used items in there
    std::vector<item_type, item_type_allocator> items;
    bucket_type_impl() : count(0), items(0) {  }
    ...
Run Code Online (Sandbox Code Playgroud)

然而线程消毒者声称在bucket_type的构造和它的第一次使用之间存在竞争,特别是当加载计数原子时.事实证明,如果你通过它的构造函数初始化std :: atomic <>,那么初始化不是原子的,因此内存位置不是原子释放的,因此对其他线程不可见,这是违反直觉的,因为它是一个原子,并且大多数原子操作默认为memory_order_seq_cst.因此,您必须在构造之后显式执行发布存储,以使用其他线程可见的值初始化原子.

是否有一些非常迫切的原因,为什么std :: atomic与值消耗构造函数不会使用发布语义初始化自己?如果没有,我认为这是一个库缺陷.

编辑:乔纳森的回答是,为什么在历史上较好,但ecatmur的回答环节对此事阿拉斯泰尔的缺陷报告,以及它是如何通过简单地添加注释说建设没有提供可视性,其他线程关闭.因此,我将奖励ecatmur.感谢所有回复的人,我认为要求一个额外的构造函数的方式很明显,它至少会在文档中脱颖而出,值得使用构造函数.

编辑2:我最终将此作为C++语言中的缺陷与委员会一起提出,并且并发部分主持人Hans Boehm认为这不是问题,原因如下:

  1. 2014年没有现有的C++编译器将消费视为与获取不同.正如您将永远不会,在现实世界的代码中,将原子传递给另一个线程而不经过一些释放/获取,原子的初始化将使用原子对所有线程可见.我觉得这很好,直到编译器赶上来,在此之前,Thread Sanitiser会对此发出警告.

  2. 如果你做不匹配的消耗,获取释放像我(我使用的版本,里面锁/消耗,外锁原子,以推测避免释放获取自旋锁的地方是不必要的),那么你是一个大足够的男孩知道你必须在施工后手动储存释放原子.这可能是一个公平的观点.

eca*_*mur 13

这是因为转换构造函数是constexpr,并且constexpr函数不能具有原子语义等副作用.

DR846中,Alastair Meredith写道:

我不确定是否使用constexpr关键字(它限制了构造函数的形式)暗示了初始化,但即使是这种情况,我认为值得明确拼写,因为推断会过于微妙.案件.

该缺陷的解决方案(劳伦斯·克劳尔)是用构造函数记录的:

[ 注意:构造不是原子的.- 尾注 ]

然后该注释扩展为当前的措辞,给出了DR1478中可能的内存竞争(通过memory_order_relaxed传递原子地址的操作)的示例.

转换构造函数需要的原因constexpr是(主要)允许静态初始化.在DR768中我们看到:

进一步的讨论:为什么ctor被标记为"constexpr"?Lawrence [Crowl]说这允许对象静态初始化,这很重要,否则初始化会出现竞争条件.

因此:使构造函数constexpr消除静态生命周期对象上的竞争条件,代价是动态生命周期对象中的竞争只发生在相当人为的情况下,因为竞争发生时动态生命周期原子对象的内存位置必须以不会导致原子对象的也与该线程同步的方式传递给另一个线程.

  • 我一直认为constexpr应该是个人超载,但我想这艘船可能已经航行了.我也不同意这是一个人为的情况,我的concurrent_unordered_map可以并发rehash,在此期间它在创建新存储桶时构造新的原子计数.然后,这些可能会在使用这些存储桶的其他线程上竞争,但我应该严重怀疑在现实世界中实际上会发生这种情况,因为代码量和两者之间的锁定和解锁. (2认同)

Jon*_*ely 12

这是一个有意的设计选择(关于它的标准警告中甚至有一个注释),我认为它是为了与C兼容而完成的.

设计C++ 11原子,以便它们也可以被WG14用于C,使用非成员函数,例如atomic_load类型atomic_int而不是C++的成员函数std::atomic<int>.在原始设计中,atomic_int类型没有特殊属性,只有通过atomic_load()其他功能才能实现原子性.在该模型atomic_init中不是原子操作,它只是初始化POD.只有后续的atomic_store(&i, 1)调用才是原子的.

最后,WG14决定以不同的方式做事,添加_Atomic使该atomic_int类型具有魔力属性的说明符.我不确定这是否意味着C atomics的初始化可能是原子的(因为它atomic_init在C11和C++ 11中被记录为非原子的),所以C++ 11规则可能是不必要的.我怀疑人们会认为有很好的性能原因可以保持初始化非原子性,正如上面的interjay的评论所说,你需要向另一个线程发送一些通知,告知构造了obejct并准备好从中读取,以便通知可以介绍必要的围栏.对std::atomic初始化执行一次,然后第二次执行对象构造可能是浪费.


cma*_*ter 6

我会说,这是因为构造永远不是一个线程通信操作:当你构造一个对象时,你用以明智的值填充以前未初始化的内存.除非构造线程明确地传达,否则另一个线程无法判断该操作是否已完成.如果你无论如何都在进行建筑竞赛,你会立即出现未定义的行为.

由于创建线程必须在允许另一个线程使用它之前显式地发布其构造值的成功,因此在同步构造函数中没有任何意义.