组合 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一般不要使使用混合原子操作的程序顺序一致。