对原子感到困惑 class:memory_order_relaxed

confused about atomic class: memory_order_relaxed

我正在研究这个网站:https://gcc.gnu.org/wiki/Atomic/GCCMM/AtomicSync,这对理解关于原子的主题非常有帮助class。

但是这个放松模式的例子很难理解:

    /*Thread 1:*/

    y.store (20, memory_order_relaxed)
    x.store (10, memory_order_relaxed)
    /*Thread 2*/
 if (x.load (memory_order_relaxed) == 10)
      {
        assert (y.load(memory_order_relaxed) == 20) /* assert A */
        y.store (10, memory_order_relaxed)
      }
 /*Thread 3*/
     if (y.load (memory_order_relaxed) == 10)
        assert (x.load(memory_order_relaxed) == 10) /* assert B */

对我来说断言 B 永远不会失败,因为 x 必须是 10 并且 y=10 因为线程 2 已经以此为条件。

但是网站上说这个例子中的任何一个断言实际上都可能失败。

To me assert B should never fail, since x must be 10 and y=10 because of thread 2 has conditioned on this.

实际上,您的论点是,由于在线程 2 中将 10 存储到 x 发生在将 10 存储到 y 之前,因此在线程 3 中也是如此。

但是,由于您只使用宽松的内存操作,因此代码中没有任何内容需要两个不同的线程就 不同 变量的修改之间的顺序达成一致。所以实际上线程 2 可能会在将 10 存储到 y 之前看到将 10 存储到 x,而线程 3 以相反的顺序看到这两个操作。

为了确保断言 B 成功,您实际上需要确保当线程 3 看到 y 的值 10 时,它还会看到线程执行的任何其他副作用在存储时间之前将 10 存储到 y 中。也就是说,您需要将 10 存储到 y 与来自 y 的 10 负载同步。这可以通过让存储执行释放并加载执行获取来完成:

// thread 2
y.store (10, memory_order_release);

// thread 3
if (y.load (memory_order_acquire) == 10)

释放操作与读取存储值的获取操作同步。现在因为线程 2 中的存储与线程 3 中的加载同步,线程 3 中加载之后发生的任何事情都会看到线程 2 中存储之前发生的任何事情的副作用。因此断言将成功。

当然,我们还需要确保断言A成功,方法是让线程1中的x.store使用release,线程2中的x.load使用acquire。

我发现通过了解可能导致原子的原因更容易理解原子,因此这里有一些背景知识。要知道,这些概念在 C++ 语言本身中并没有以任何方式陈述,而是事情之所以如此的一些可能原因。

编译器重新排序

编译器通常在优化时会选择重构程序,只要其效果在单线程程序上相同。这是通过使用原子来规避的,它会告诉编译器(除其他外)变量可能随时更改,并且它的值可能会在其他地方读取。

形式上,原子确保一件事:不会有数据竞争。也就是说,访问该变量不会使您的计算机爆炸。

CPU 重新排序

CPU 可能会在执行指令时重新排序指令,这意味着指令可能会在硬件级别重新排序,与您编写程序的方式无关。

缓存

最后还有缓存的影响,它是更快的内存,有点包含全局内存的部分副本。缓存并不总是同步的,这意味着它们并不总是就 "correct" 的内容达成一致。不同的线程可能不会使用相同的缓存,因此,它们可能不会就变量的值达成一致。

回到问题

以上内容几乎就是 C++ 对此事的表述:除非 明确地 另有说明,否则每条指令的副作用的顺序是完全和完全未指定的。从不同的线程看甚至可能不一样。

形式上,副作用排序的保证称为happens-before关系。除非副作用先于发生,否则不会。松散地,我们只是说称之为同步。

现在,memory_order_relaxed 是什么?它告诉编译器停止干预,但不要担心 CPU 和缓存(可能还有其他东西)的行为。因此,您看到 "impossible" 断言的一种可能性可能是

  1. 线程 1 将 20 存储到 y,然后将 10x 存储到其缓存中。
  2. 线程 2 读取新值并将 10y 存储到其缓存中。
  3. 线程 3 没有读取线程 1 的值,而是读取了线程 2 的值,然后断言失败。

这可能与现实中发生的情况完全不同,重点是任何事情都可能发生。

要确保多次读取和写入之间的 happens-before 关系,请参阅

另一个提供happens-before关系的构造是std::mutex,这就是为什么它们没有这种疯狂。

你的问题的答案是 C++ 标准。

[intro.races] 部分非常清楚(这不是规范文本类型的规则:形式主义的一致性通常会损害可读性)。

我读了很多关于内存顺序的书和tuto,但它让我很困惑。 我终于阅读了 C++ 标准,[intro.multithread] 部分是我发现的最清楚的部分。花时间仔细阅读(两遍)可能会节省您一些时间!

您问题的答案在[intro.races]/4:

All modifications to a particular atomic object M occur in some particular total order, called the modification order of M. [ Note: There is a separate order for each atomic object. There is no requirement that these can be combined into a single total order for all objects. In general this will be impossible since different threads may observe modifications to different objects in inconsistent orders. — end note ]

您期待所有原子操作的单一总订单。有这样的顺序,但仅适用于 memory_order_seq_cst 的原子操作,如 [atomics.order]/3:

中所述

There shall be a single total order S on all memory_order_seq_cst operations, consistent with the “happens before” order and modification orders for all affected locations [...]