memory_order_consume和memory_order_acquire之间的差异

问题描述 投票:14回答:2

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

线程1:

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

线程2:

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

据说,如果所有存储都是release并且所有加载都是acquire,则线程2中的断言不会失败。这对我来说很清楚(因为线程1中x的存储与线程2中x的负载同步)。

但是现在出现了我不了解的部分。还可以说,如果所有存储都是release,而所有装入都是consume,则结果是相同的。 y的负载是否可能先于x的负载被吊起(因为这些变量之间没有依赖性)?这将意味着线程2中的断言实际上可能会失败。

c++ c atomic memory-model stdatomic
2个回答
12
投票

C11标准的裁定如下。

5.1.2.4多线程执行和数据争用

  1. 评估A是依赖关系排序前 16)评估B,如果:

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

    -对于某些评估X,在X和X对B进行依赖之前,A是依赖关系排序的。

  2. 评估A 线程间发生在之前

  3. 评估B,如果A与B同步,A在B之前是依序排序的,或者对于某些评估X:

    — A与X同步,并且X在B之前排序,

    — A在X和X线程间发生在B之前进行排序,或者

    —一个线程间发生在X之前,而X线程间发生在B之前。

  4. 注7“线程间发生在...之前”关系描述了“在...之前排序”,“与...同步”和“依赖顺序在...之前”关系的任意串联,例外

  5. 。第一个例外是不允许串联以“先于依序进行依存关系”结尾,再以“先后依序”结尾。出现此限制的原因是,参与“之前有依存关系排序”关系的消费操作仅提供关于该消费操作实际承载依赖项的操作的排序。此限制仅适用于这种连接的最后一个结果是,任何后续的释放操作都将为先前的消耗操作提供所需的排序。第二个例外是,串联不得完全由“先后排序”组成。这种限制的原因是:(1)允许“线程间发生在”之前被暂时关闭;(2)下文定义的“发生在……之间”关系提供了完全由“先于序列”组成的关系”。
  6. 评估A

    发生在…之前

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

    可见副作用A

  9. 对M的值计算B满足条件:

    A发生在B之前,

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

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

    ((已添加重点)


在下面的评论中,我将缩写如下:

  • 依赖关系排序前:
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),我们有:
    A

    SeqB

    B DOB C SeqB D
    这是一个以DOB结尾,然后是SeqB的串联。尽管(16)说了什么,但这是由(17)决定的[[明确成为

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

    然后得出结论,D对D不可见,因为不满足(19)的条件之一。断言可能会失败。

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


    ((提前一些时间)线程2的分支预测变量猜测将采用if

      线程2接近预测的分支并开始推测性获取。
    1. 线程2无序,并且从0xGUNK推测加载y(评估D)。 (也许还没有从缓存中清除它?)。
    2. 线程1将20存储到y(评估A)
    3. 线程1将10存储到x(评估B)
    4. 线程2从10加载x(评估C)
    5. 线程2确认已取得if
    6. 线程2的y == 0xGUNK的推测性负载已落实。
    7. 线程2断言失败。
    8. 之所以允许评估D在C之前重新排序是因为
    9. consume
    10. 确实

      not禁止了它。这不同于acquire-load

    ,后者防止按程序顺序对其进行任何加载/存储afterbefore中重新排序。再次,在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; }

    该工具报告

    two一致的,无竞赛的场景,即:

    Consume, Success Scenario

    成功读取y=20,并且

    Consume, Failure Scenario

    读取“陈旧”的初始化值y=30。写意圈是我的。

    相反,当使用mo_acquire进行加载时,CppMem仅报告

    one

    一致的无竞争场景,即正确的场景:

    Acquire, Success Scenario

    其中读取y=20

    两者都在原子存储上建立可传递的“可见性”顺序,除非它们已被发出memory_order_relaxed。如果线程使用以下一种模式读取原子对象x,则可以确保它可以看到对所有原子对象y的所有修改,这些修改在写入x之前已经完成。
    例如,“ acquire”和“ consume”之间的区别在于对某些变量z的非原子写操作的可见性。对于acquire

    all

    可见,无论是原子写入还是不原子写入。对于consume,仅保证原子原子可见。

    thread 1 thread 2 z = 5 ... store(&x, 3, release) ...... load(&x, acquire) ... z == 5 // we know that z is written z = 5 ... store(&x, 3, release) ...... load(&x, consume) ... z == ? // we may not have last value of z


    6
    投票
    例如,“ acquire”和“ consume”之间的区别在于对某些变量z的非原子写操作的可见性。对于acquire

    all

    © www.soinside.com 2019 - 2024. All rights reserved.