原子<t> .load()与std :: memory_order_release </t>

时间:2013-12-05 10:55:23

标签: multithreading c++11 atomicity

当编写使用新引入的线程同步原语的C ++ 11代码来使用宽松的内存排序时,通常会看到

std::atomic<int> vv;
int i = vv.load(std::memory_order_acquire);

vv.store(42, std::memory_order_release);

我很清楚为什么这是有道理的。

我的问题是:vv.store(42, std::memory_order_acquire)vv.load(std::memory_order_release)组合是否也有意义?在哪种情况下可以使用它们?这些组合的语义是什么?

4 个答案:

答案 0 :(得分:5)

这根本不被允许。 C ++(11)标准对可以对加载/存储操作施加的内存顺序约束有要求。

对于载荷(§29.6.5):

  

要求:订单参数不应为memory_order_release,也不应为memory_order_acq_rel

对于商店:

  

要求:订单参数不应为memory_order_consumememory_order_acquire,也不应为memory_order_acq_rel

答案 1 :(得分:3)

这些组合没有任何意义,也不允许使用它们。

获取操作将先前的非原子写入或副作用与释放操作同步,以便在实现获取(加载)时,在发布(存储)之前发生的所有其他存储(效果)也是可见的(对于线程获得发布的相同原子。)

现在,如果你可以(并且会做)获取商店和发布加载,它该怎么办?获取操作应该与哪个存储同步?本身?

答案 2 :(得分:2)

C / C ++ / LLVM内存模型足以进行同步 确保在访问之前可以访问数据的策略 它。虽然这涵盖了大多数常见的同步原语,但很有用 通过在较弱的上建立一致的模型可以获得属性 保证。

最大的例子是seqlock。 它依赖于“推测性地”读取可能不存在的数据 一致的状态。因为允许读取与写入竞争, 读者不会阻止编写者 - 一种在Linux中使用的属性 内核,即使用户进程也允许更新系统时钟 正在反复阅读它。 seqlock的另一个优点是 现代SMP拱门与读者数量完美匹配: 因为读者不需要任何锁,他们只需要 共享访问缓存行。

seqlock的理想实现将使用类似的东西 阅读器中的“释放负载”,这在任何专业都没有 编程语言。内核使用a full read fence来解决这个问题, 它可以跨架构进行扩展,但是doesn't achieve optimal performance

答案 3 :(得分:1)

  

执行组合vv.store(42,std :: memory_order_acquire)和   vv.load(std :: memory_order_release)也有意义吗?

从技术上讲,它们在形式上是被禁止的,但是了解这一点并不重要,除了编写C ++代码。

它们根本无法在模型中定义,即使您不编写代码,也要了解和理解这一点很重要。

请注意,禁止使用这些值是一个重要的设计选择:如果编写your_own::atomic<>类,则可以选择允许这些值并将其定义为等同于宽松操作。

了解设计空间很重要;您不必对所有C ++线程原始设计选择有太多的尊重,其中某些选择纯粹是任意的。

  

在哪种情况下可以使用它们?的语义是什么   这些组合?

没有人,作为,您必须了解读不是写的基本概念(花了我一段时间才可以得到它)。您只有在了解了非线性执行后才能声称自己了解这种非线性执行。

在没有异步信号的非线程程序中,所有步骤都是顺序的,读取不写入是没有关系的:如果您安排序列点到一个对象,则对对象的所有读取都可以重写值。请注意,如果您允许写入自己的常量值(实际上,只要内存为R / W,就可以)。

因此,在这样的级别上,读写之间的区别不是那么重要。您可以设法定义仅基于语义的操作,该操作既是对存储单元的读取又是对存储单元的读写,这样就可以写入常量,而读取不使用的无效值是可以的。

我当然不建议这样做,因为模糊读写之间的区别非常难看。

但是对于多线程,您确实不想写只读取的数据:不仅会创建数据争用(当您回写旧值时,您可以任意声明不重要),也不会当写入操作更改共享对象的缓存行的状态时,映射到CPU世界视图。读取不是写入这一事实对于多线程程序的效率至关重要,远比单线程程序有效。

在抽象级别上,对原子的存储操作是一种修改,因此它是其修改顺序的一部分,而负载不是:负载仅指向修改顺序中的某个位置(负载可以看到原子存储的值或初始值,即在进行所有原子修改之前在构造时建立的值。

修改是相互排序的,而负载是,仅针对修改。 (您可以将负载查看为完全同时发生。)

获取和释放操作是关于创建历史记录(过去)并进行通信:对对象的释放操作使您过去的原子对象成为过去,而获取操作使过去的事物成为您的过去。

不是原子RMW的修改看不到先前的值;另一方面,先包含一个负载再再包含一个存储(在一个或两个原子上)的算法会看到一些先前的值,但通常不能保证看到修改之前按修改顺序剩下的值,因此获取负载X后跟释放存储区Y会传递历史记录,并将过去(在某个时候通过X看到的另一个释放操作在另一个线程处)的过去与原子变量相关的过去(由Y构成)的一部分剩下的时间)。

RMW在语义上不同于获取然后发布,因为在发布和获取之间的历史记录中从来没有“空格”。这意味着仅使用acq + rel RMW操作的程序始终保持顺序一致,因为它们可以完全消除与之交互的所有线程的使用。

因此,如果您想要acq + rel加载或存储,只需执行RMW读或RMW写操作:

  • acq + rel负载是RMW回写相同的值
  • acq + rel存储正在RMW取消了原始值

您可以编写自己的(强)原子类,该类用于(强)加载和(强)存储:在逻辑上将其定义为您的类将使所有操作(甚至是负载)成为操作的操作历史的一部分。 (强)原子对象。因此,“(强)存储”可以观察到“(强)负载”,因为它们既是(原子)修改又是对底层正常原子对象的读取。

请注意,对于使用宽松原子操作的程序,在此类“强原子”对象上的acq_rel操作集比在正常原子上的seq_cst操作集的意图保证具有严格更强的保证: seq_cst的设计者的意图是,使用seq_cst不会使使用混合原子操作的程序通常顺序一致。