对原子类感到困惑:memory_order_relaxed

时间:2017-09-06 02:12:15

标签: c++ multithreading thread-safety atomic memory-model

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

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

    /*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已经以此为条件。

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

3 个答案:

答案 0 :(得分:6)

  

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

实际上,你的论点是,因为在第2个线程中,10进入x的商店发生在10存储到y之前,在线程3中必须是这种情况。

但是,由于您只使用宽松的内存操作,因此代码中没有任何内容需要两个不同的线程来同意不同变量的修改之间的顺序。因此,线程2确实可以在10存储到x之前将10的存储看到y,而线程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中存储之前发生的任何事情的副作用。因此断言将成功。

当然,我们还需要通过使线程1中的x.store使用release并且线程2中的x.load使用acquire来确保断言A成功。

答案 1 :(得分:2)

我发现原子学更容易理解可能导致它们的原因,所以这里有一些背景知识。要知道这些概念在C ++语言本身中没有任何说明,但是可能的原因可能就是它们的存在方式。

编译器重新排序

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

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

CPU重新排序

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

缓存

最后有缓存的效果,这是更快的内存,sorta包含全局内存的部分副本。缓存并不总是同步,这意味着他们并不总是同意什么是“正确的”。不同的线程可能没有使用相同的缓存,因此,他们可能不同意变量的值。

回到问题

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

形式上,副作用排序的保证称为发生之前关系。除非副作用发生在之前,否则不是。很简单,我们只是称之为同步。

现在,memory_order_relaxed是什么?它告诉编译器停止插入,但不要担心CPU和缓存(以及可能的其他东西)的行为。因此,为什么你看到“不可能的”断言的一种可能性可能是

  1. 主题1将20存储到y,然后将10存储到x到其缓存。
  2. 线程2读取新值并将10存储到y到其缓存中。
  3. 线程3没有读取线程1中的值,但读取线程2的值,然后断言失败。
  4. 这可能与现实中的情况完全不同,重点是任何都可能发生。

    要确保多次读写之间发生在之前的关系,请参阅Brian's answer

    另一个提供发生 - 之前关系的构造是std::mutex,这就是为什么它们没有这种疯狂的原因。

答案 2 :(得分:2)

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

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

我读过很多书和tuto,它们都是关于记忆顺序的主题,但它让我很困惑。 最后我已经阅读了C ++标准,[intro.multithread]部分是我发现的最清晰的部分。花时间仔细阅读(两次)可以节省一些时间!

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

  

对特定原子对象M的所有修改都以某种特定的总顺序出现,称为修改   M.的顺序[注意:每个原子对象都有一个单独的顺序。没有要求这些可以   可以合并为所有对象的单个总订单。一般来说,由于不同的线程,这是不可能的   可能会以不一致的顺序观察对不同对象的修改。 - 结束说明]

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

  

所有memory_order_seq_cst次操作都应该有一个总订单S,与“发生的一致”一致   在“所有受影响地点的订单和修改订单[...]

之前