在proposed boost::concurrent_unordered_map的线程清理过程中出现了一些非常奇怪的事情,并且是recounted at this blog post。简而言之,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) { }
...
然而线程消毒者声称在bucket_type的构造和它的第一次使用之间存在竞争,特别是当加载计数原子时。事实证明,如果你初始化一个std :: atomic&lt;&gt;通过它的构造函数that initialisation is not atomic,因此内存位置不是原子释放的,因此对其他线程不可见,这是违反直觉的,因为它是一个原子,并且大多数原子操作默认为memory_order_seq_cst。因此,您必须在构造之后显式执行发布存储,以使用其他线程可见的值初始化原子。
有一些非常迫切的原因,为什么std :: atomic与值消耗构造函数不会使用发布语义初始化自己?如果没有,我认为这是一个库缺陷。
编辑:Jonathan的答案对历史来说更好,但是ecatmur回答了Alastair关于此事的缺陷报告的链接,以及如何它只是通过添加一个注释来关闭,说构造提供其他线程无法看到。因此,我会给出ecatmur的答案。感谢所有回复的人,我认为要求一个额外的构造函数的方式很明显,它至少会在文档中脱颖而出,与值消耗构造函数有一些不同寻常的东西。
编辑2:我最终将此作为C ++语言中的缺陷与委员会一起提出,而并发部分主席Hans Boehm认为这不是问题,原因如下:
2014年没有现有的C ++编译器将消费视为与获取不同。正如您将永远不会,在现实世界的代码中,将原子传递给另一个线程而不经过一些释放/获取,原子的初始化将使用原子对所有线程可见。我觉得这很好,直到编译器赶上来,在此之前,Thread Sanitiser会对此发出警告。
如果你像我一样做了不匹配的消费 - 获取 - 释放(我正在使用一个release-inside-lock / consume-outside-lock原子来推测性地避免一个release-acquire spinlock,它就是不必要的)那么你是一个足够大的男孩知道你必须在施工后手动存储释放原子。这可能是一个公平的观点。
答案 0 :(得分: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原子的初始化可能是原子的(就像它在C11中atomic_init
而C ++ 11被记录为非原子的),所以也许是C ++ 11规则是不必要的。我怀疑人们会认为保持初始化非原子性有良好的性能原因,正如上面的interjay评论所说,你需要向另一个线程发送一些通知,告知构造了obejct并准备好从中读取,这样通知就可以引入必要的围栏。为std::atomic
初始化执行一次,然后第二次执行该对象的构建可能是浪费。
答案 1 :(得分:6)
我说,这是因为构造永远不是一个线程通信操作:当你构造一个对象时,你用以明智的值填充以前未初始化的内存。除非构造线程明确地传达,否则另一个线程无法判断该操作是否已完成。如果你无论如何都要进行建筑竞赛,你会立即出现未定义的行为。
由于创建线程必须在允许另一个线程使用它之前显式发布其构造值的成功,因此同步构造函数根本没有意义。