2

在编写使用新引入的线程同步原语以利用宽松的内存排序的 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

4 回答 4

5

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

对于负载(§29.6.5):

要求: order 参数不应为memory_order_releasenor memory_order_acq_rel

对于商店:

要求: order 参数不应是memory_order_consume, memory_order_acquire, 也不memory_order_acq_rel

于 2013-12-05T12:02:15.633 回答
3

这些组合没有任何意义,也不被允许。

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

现在,如果你可以(并且愿意)获取存储和释放加载,它应该怎么做?获取操作应该与哪个存储同步?本身?

于 2013-12-05T12:06:29.507 回答
3

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

最大的例子是seqlock。它依赖于“推测性地”读取可能不处于一致状态的数据。因为允许读取与写入竞争,所以读取器不会阻止写入器——Linux 内核中使用的一个属性,即使用户进程重复读取它也允许更新系统时钟。seqlock 的另一个优点是,在现代 SMP 架构上,它可以完美地随读取器的数量而扩展:因为读取器不需要任何锁,它们只需要对缓存行的共享访问。

seqlock 的理想实现将在阅读器中使用类似“释放负载”的东西,这在任何主要的编程语言中都不可用。内核通过一个完整的读取栅栏来解决这个问题,它可以很好地跨架构扩展,但没有达到最佳性能

于 2017-11-23T19:33:54.047 回答
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 load 是 RMW 写回相同的值
  • acq+rel 存储是 RMW 消除原始值

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

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

于 2019-12-08T22:47:18.503 回答