memory_order_consume和memory_order_acquire之间的区别

时间:2015-08-13 16:16:48

标签: c atomic c11 memory-model

我对GCC-Wiki article有疑问。在标题"总结摘要"给出了以下代码示例:

主题1:

y.store (20);
x.store (10);

主题2:

if (x.load() == 10) {
  assert (y.load() == 20)
  y.store (10)
}

据说,如果所有商店都发布并且所有商品获取,则线程2中的断言不会失败。这对我来说很清楚(因为线程1中x的存储与线程2中x的加载同步)。

但现在出现了我不明白的部分。还有人说,如果所有商店发布且所有商品消费,结果都是相同的。来自y的负载是否有可能在来自x的负载之前被提升(因为这些变量之间没有依赖关系)?这意味着线程2中的断言实际上可能会失败。

2 个答案:

答案 0 :(得分:11)

C11标准的裁决如下:

5.1.2.4多线程执行和数据竞争

  
      
  1. 评估A是 16之前依赖性排序评估B如果:

         

    - A对原子对象M执行释放操作,并且在另一个线程中,B对M执行消耗操作并读取由A开头的释放序列中的任何副作用写入的值,或

         

    - 对于某些评估X,A在X之前是依赖排序的,而X对B具有依赖性。

  2.   
  3. 评估A 线程发生在评估B之前如果A与B同步,A在B之前依赖排序,或者对于某些评估X:

         

    - 与X同步,X在B之前排序,

         

    - 在X和X之间的线程发生在B之前或

    之前对A进行排序      

    - 在X和X之间的线程发生在B之前发生了一个内部线程。

  4.   
  5. 注7''线程之间发生''关系描述''之前排序'',''与''同步'和''关系之前依赖顺序'的任意连接,有两个例外。第一个例外是串联不允许以''依赖顺序排列''结尾,然后''排序之前''结束。这种限制的原因是参与“依赖关系排序之前”关系的消费操作仅针对此消费操作实际带有依赖性的操作提供排序。此限制仅适用的原因到这种连接的结尾是任何后续的释放操作将为先前的消费操作提供所需的排序。第二个例外是串联不允许完全由''之前排序'组成。这种限制的原因是(1)允许''线程发生在''之前发生传递关闭;(2)''之前发生'''关系',定义如下,定义完全由''之前排序的关系''。

  6.   
  7. 评估A 发生在评估B之前如果A在B 之前排序或在B之前发生线程间。

  8.   
  9. 对象M上的可见副作用A 相对于M的值计算B满足条件:

         

    - A发生在B之前,

         

    - 没有其他副作用X到M,使得A发生在X之前,而X发生在B之前。

         

    由评估B确定的非原子标量对象M的值应为可见副作用A存储的值。

  10.   

(强调补充)

在下面的评论中,我将在下面简述如下:

  • 之前订购的依赖关系: DOB
  • 之间的线程发生在: ITHB
  • 之前发生: HB
  • 之前排序: SeqB

让我们回顾一下这是如何适用的。我们有4个相关的内存操作,我们将其命名为评估A,B,C和D:

主题1:

y.store (20);             //    Release; Evaluation A
x.store (10);             //    Release; Evaluation B

主题2:

if (x.load() == 10) {     //    Consume; Evaluation C
  assert (y.load() == 20) //    Consume; Evaluation D
  y.store (10)
}

为了证明断言从不跳过,我们实际上试图证明 A在D 总是一个可见的副作用。根据 5.1.2.4 (15),我们有:

  

SeqB B DOB C SeqB D

这是以DOB结尾的串联,后跟SeqB。这是显式由(17)明确统治是ITHB连接,尽管(16)说。

我们知道由于A和D不在同一执行线程中,因此A不是SeqB D;因此,对于HB中的(18)中的两个条件都不满足,并且A不是HB D.

由此得出A对D不可见,因为不符合(19)的条件之一。断言可能会失败。

然后,如何展示here, in the C++ standard's memory model discussionhere, Section 4.2 Control Dependencies

  1. (提前一段时间)线程2的分支预测器猜测将if被采用。
  2. 线程2接近预测的分支并开始推测性获取。
  3. 线程2无序并从0xGUNK推测性地加载y(评估D)。 (也许它还没有从缓存中逐出?)。
  4. 主题1将20存储到y(评估A)
  5. 主题1将10存储到x(评估B)
  6. 主题2从10(评估C)
  7. 加载x
  8. 线程2确认if已被采纳。
  9. 线程2的y == 0xGUNK投机负载已提交。
  10. 线程2失败断言。
  11. 允许在C之前重新排序评估D的原因是因为使用 禁止它。这与 acquire-load 不同, acquire-load 可以防止在之前重新排序之后 之前的任何加载/存储。同样,5.1.2.4(15)声明,参与“依赖性排序之前”关系的消费操作仅提供有关此消费操作实际带有依赖性的操作的排序,以及绝对不是两个负载之间的依赖关系。

    CppMem验证

    CppMem是一个有助于探索C11和C ++ 11内存模型下共享数据访问方案的工具。

    对于以下代码,它近似于问题中的场景:

    int main() {
      atomic_int x, y;
      y.store(30, mo_seq_cst);
      {{{  { y.store(20, mo_release);
             x.store(10, mo_release); }
      ||| { r3 = x.load(mo_consume).readsvalue(10);
            r4 = y.load(mo_consume); }
      }}};
      return 0; }
    

    该工具报告两个一致,无种族的方案,即:

      

    Consume, Success Scenario

    成功阅读y=20

      

    Consume, Failure Scenario

    其中"陈旧"读取初始化值y=30。手绘圈是我的。

    相比之下,当mo_acquire用于加载时,CppMem仅报告一个一致的无种族场景,即正确的场景:

      

    Acquire, Success Scenario

    其中y=20被读取。

答案 1 :(得分:7)

两者都建立了传递性和可见性"原子商店的订单,除非它们已经发出memory_order_relaxed。如果某个线程使用其中一种模式读取原子对象x,则可以确保它在写入{{1}之前看到所有原子对象y的所有修改已知}。

"获得"之间的区别并且"消费"比如,对某些变量x的非原子写入的可见性。对于z 所有写入,无论是否为原子,都是可见的。对于acquire,只保证原子可见。

consume