【问题标题】:Do I understand the semantics of std::memory_order correctly?我是否正确理解 std::memory_order 的语义?
【发布时间】:2021-11-22 13:43:13
【问题描述】:

c++reference.com 表示关于memory_order::seq_cst

具有此内存顺序的加载操作执行获取操作,存储执行释放操作,读取-修改-写入执行获取操作和释放操作,加上存在一个所有线程观察所有线程的单一总顺序修改顺序相同。

[ Q1 ]:这是否意味着顺序通过所有(其他+此)atomic_vars 的每个操作都使用 memory_order::seq_cst?

[Q2]:并且releaseacquirerel_acq不包括在“单总订单”中?

我知道seq_cst 相当于其他三个具有写入、读取和write_read 操作,但我很困惑seq_cst 是否也可以订购其他 atomic_var,而不仅仅是相同的 var。

【问题讨论】:

  • @appleapple 谢谢你。我想我理解正确,但我的英语不太好,我担心我可能错了。所以我想检查我是否正确。再次感谢。

标签: c++ multithreading memory-barriers stdatomic memory-model


【解决方案1】:

cppreference 只是 C++ 标准的一个总结,有时它的文字不够精确。实际的标准草案清楚地表明:final C++20 working draft N4681 在 atomics.order 中声明,par。 4(第 1525 页):

所有memory_order::seq_cst 操作(包括栅栏)都有一个总订单 S,它满足以下约束 [...]

这清楚地表明所有 seq_cst 操作,而不仅仅是针对特定对象的所有操作。

并且注释 6 和 7 进一步强调该顺序不适用于较弱的记忆顺序:

6 [注:我们不要求 S 与“发生在之前”(6.9.2.1)一致。这样可以更高效 在某些机器架构上实现 memory_order::acquire 和 memory_order::release。 当这些与 memory_order::seq_cst 访问混合时,它会产生令人惊讶的结果。 ——尾注]

7 [注意:memory_order::seq_cst 仅确保没有数据竞争的程序的顺序一致性 并专门使用 memory_order::seq_cst 原子操作。任何使用较弱的排序都会失效 除非非常小心,否则此保证。很多情况下,memory_order::seq_cst 原子操作

【讨论】:

  • 谢谢!我应该阅读标准文档以更清楚地了解副作用。谢谢。
  • 我添加了一个草稿链接(它有一个不同的数字——4680 是协程扩展提案)。对此问题感兴趣的其他人可能也会感兴趣。
【解决方案2】:

我发现这部分不完整:

具有此内存顺序的加载操作执行获取操作, store 执行释放操作,read-modify-write 执行 一个获取操作和一个释放操作,加上一个总数 存在所有线程观察所有修改的顺序 相同的顺序。

如果这些东西(释放存储、获取加载和总存储顺序)实际上足以提供顺序一致性,这意味着释放和获取操作本身的顺序将比实际更严格。

让我们看看下面的反例:

CPU1:
   a = 1 // release store
   int r1 = b // acquire load

然后根据上面对 SC 的定义(以及已知的属性顺序一致性必须符合名称),我会假设 a 的存储和 b 的负载不能重新排序:

  • 我们有一个发布存储和一个获取加载
  • 我们(可以)对所有加载/存储有一个总订单

所以我们已经满足了上述顺序一致性的定义。

但是可以重新排序释放存储,然后是获取加载到不同的地址。典型的例子是 Dekker 算法。因此,上述对 SC 的定义被打破,因为它缺少内存顺序需要保留程序顺序。除了编译器搞砸了事情之外,这种违规的典型原因是大多数现代 CPU 拥有的存储缓冲区可能会导致较旧的存储被重新排序,并将新的加载到不同的地址。

单个总顺序与 CPU 本地指令重新排序不同,您可以使用例如存储缓冲区。它实际上意味着某个操作在内存顺序中生效的时刻,没有人应该能够不同意这一点。对此的标准试金石是独立读取独立写入 (IRIW):

CPU1:
   A=1
CPU2:
   B=1
CPU3:
   r1=A
   [LoadLoad]
   r2=B
CPU4:
   r3=B
   [LoadLoad]
   r4=A

那么 CPU3 和 CPU4 会不会以不同的顺序看到不同地址的存储?如果答案是肯定的,则不存在加载/存储的总订单。

加载/存储没有总顺序的另一个原因是存储到加载转发 (STLF)。

CPU1:
   A=1
   r1=A
   r2=B

CPU2:
   B=1
   r3=B
   r4=A

r1=1, r2=0, r3=1 和 r4=0 有可能吗?

在 X86 上,由于存储到负载转发,这是可能的。因此,如果 CPU1 先存储 A,然后加载 A,则 CPU 必须在存储缓冲区中查找 A 的值。这导致 A 的存储不是原子的;本地 CPU 可以及早看到存储,结果是不存在加载/存储的总顺序。

因此,不是对所有加载/存储进行总排序,而是将其简化为对存储的总排序,这就是 X86 为其内存模型(总存储排序)命名的方式。

[编辑] 做了一些澄清。我清理了一些文本并清理了原始示例,因为它具有误导性。

【讨论】:

  • 问题是引用一段描述memory_order::seq_cst的段落。您将其削弱为memory_order::releasememory_order::acquire。 “单总订单”属性只是 seq_cst 的一个特性;这就是它如此昂贵的原因,因为即使是纯存储也必须耗尽存储缓冲区(至少在 seq_cst 加载之前,例如在 ARMv8 上)。该段的重点是说明store(seq_cst) 是一个类似于store(release) 的释放操作,以及 是总订单的一部分。并不是说所有的发布操作都有这个属性。
  • 这里需要说明一点,它的cppreference总结并没有说所有观察者同意的修改顺序与每个线程中的程序顺序兼容。并且加载和存储都是 seq_cst 操作形成的总订单的一部分。您引用的段落暗示没有 StoreLoad 重新排序,只有 x86 TSO。 (以相同的顺序观察所有修改)是的,seq_cst 确实意味着存在总存储顺序(对于 seq_cst 阅读器),但是在总结这一点时最好留下作为所有操作的总订单的一部分。
  • "这里有一点需要说明,它的cppreference摘要没有说明所有观察者同意的修改顺序与每个线程中的程序顺序兼容。 " 没错。
  • 此外,总订单不只是 SC 的属性。例如。 IBM 370 还对所有加载和存储进行了总排序,但它允许将较旧的存储重新排序,并将较新的加载重新排序到不同的地址。只有它不允许 STLF;在可以执行对同一地址的加载之前,它需要存储以使其全局可见。因此,加载/存储 + 获取/释放的总订单不会给 SC。您需要保留 PO,这是我对上述描述的主要抱怨。
  • 回答“完全正确”是什么意思。您的回答仍然是“然后根据上面的定义,我认为 a 的存储和 b 的负载不能重新排序:”但这没有任何意义,因为您使用了 mo_releasemo_acquire 所以段落你引用的关于你的代码块没有什么可说的。 (正如您刚刚讨论的 IBM370,它所说的不会阻止 StoreLoad 重新排序,只是保证有 some 总顺序可能与程序顺序不兼容。实际上更弱,因为它不提到负载是总订单的一部分。)
猜你喜欢
  • 2019-09-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-29
  • 2011-05-05
  • 2020-11-22
  • 2021-07-09
相关资源
最近更新 更多