繁体   English   中英

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

[英]Why doesn't std::atomic initialisation do atomic release so other threads can see the initialised value?

提议的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) {  }
    ...

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

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

编辑: Jonathan的答案对于历史来说更好,但是ecatmur的回答链接到Alastair关于此事的缺陷报告,以及如何通过简单地添加注释来表示构造提供其他线程的可见性。 因此,我将奖励ecatmur。 感谢所有回复的人,我认为要求一个额外的构造函数的方式很明显,它至少会在文档中脱颖而出,值得使用构造函数。

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

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

  2. 如果你像我一样做了不匹配的消费 - 获取 - 释放(我正在使用一个release-inside-lock / consume-outside-lock原子来推测性地避免发布 - 获取自旋锁,这是不必要的)那么你就是一个大的足够的男孩知道你必须在施工后手动储存释放原子。 这可能是一个公平的观点。

这是因为转换构造函数是constexprconstexpr函数不能具有原子语义等副作用。

DR846中 ,Alastair Meredith写道:

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

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

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

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

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

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

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM