与“(简单)发生在之前”相比,“强烈发生在之前”有何意义?

Hol*_*Cat 25 c++ concurrency multithreading language-lawyer c++20

该标准定义了几个“发生在之前”关系,这些关系将古老的“之前排序”扩展到多个线程:

\n
\n

[intro.races]

\n

11 评估 A简单地发生在评估 B 之前,如果

\n

(11.1) \xe2\x80\x94 A 在 B 之前排序,或者
\n(11.2) \xe2\x80\x94 A 与 B 同步,或者
\n(11.3) \xe2\x80\x94 A 只是发生在 X 之前,并且X 只是发生在 B 之前。

\n

[注10:在没有消耗操作的情况下,发生在之前和简单发生在关系之前是相同的。\xe2\x80\x94 尾注]

\n

12 评估 A强烈发生在评估 D 之前,如果:

\n

(12.1) \xe2\x80\x94 A 在 D 之前排序,或者
\n(12.2) \xe2\x80\x94 A 与 D 同步,并且 A 和 D 都是顺序一致的原子操作\n([atomics.order] ),或
\n(12.3) \xe2\x80\x94 存在评估 B 和 C,使得 A 在 B 之前排序,B 仅发生在 C 之前,并且 C 在 D 之前排序,或者
\n(12.4) \xe2\ x80\x94 存在一个评估 B,使得 A 强烈发生在 B 之前,并且 B 强烈发生在 D 之前。

\n

[注释 11:非正式地,如果 A 强烈发生在 B 之前,则在所有上下文中 A 似乎都在 B 之前被评估。强烈发生在排除消耗操作之前。\xe2\x80\x94 尾注]

\n
\n

(粗体我的)

\n

两者之间的差异似乎非常微妙。对于匹配对或释放获取操作来说,“强烈发生在之前”永远不会成立(除非两者都是 seq-cst),但它仍然以某种方式尊重释放获取同步,因为在释放之前排序的操作“强烈发生在之前” \' 匹配获取后排序的操作。

\n

为什么这种差异很重要?

\n

C++20 中引入了“强烈发生在之前”,在 C++20 之前,“简单发生在之前”过去被称为“强烈发生在之前”。为什么要引入它?

\n

[atomics.order]/4表示所有 seq-cst 操作的总顺序与“强烈发生在之前”一致。

\n

这是否意味着它与“简单发生在之前”不一致?如果是这样,为什么不呢?

\n
\n

我忽略了普通的“发生在之前”,因为它与“简单发生在之前”的不同之处仅在于它的处理,暂时不鼓励memory_order_consume使用它,因为显然大多数(所有?)主要编译器都会对待它作为。memory_order_acquire

\n

我已经看过这个问答,但它没有解释为什么存在“强烈发生在”之前,并且没有完全解决它的含义(它只是指出它不尊重释放获取同步,但情况并不完全如此)。

\n
\n

找到了介绍“之前就发生过”的提案。

\n

我不完全理解它,但它解释了以下内容:

\n
    \n
  • “强烈发生在”之前是“简单发生在”的弱化版本。
  • \n
  • 仅当 seq-cst 与 aqc-rel 在同一变量上混合时才能观察到差异(我认为,这意味着当获取负载从 seq-cst 存储读取值时,或者当 seq-cst 负载从发布商店)。但我仍然不清楚将两者混合的确切效果。
  • \n
\n

Hol*_*Cat 11

这是我目前的理解,可能不完整或不正确。如有核实,我们将不胜感激。


C++20 重命名strongly happens beforesimply happens before,并为 引入了一个新的、更宽松的定义strongly happens before,这减少了排序。

Simply happens before用于推断代码中是否存在数据争用。(实际上,这就是简单的“发生在之前”,但是在没有消耗操作的情况下,两者是等效的,标准不鼓励使用消耗操作,因为大多数(所有?)主要编译器将它们视为获取。)

较弱的strongly happens before用于推理 seq-cst 操作的全局顺序。


此更改是在提案P0668R5:修订 C++ 内存模型中引入的,该提案基于 Lahav 等人的论文Repairing Sequential Consistency in C/C++11(我没有完全阅读)。

该提案解释了进行更改的原因。长话短说,大多数编译器在 Power 和 ARM 架构上实现原子的方式在极少数边缘情况下被证明是不符合标准的,修复编译器会带来性能成本,因此他们修复了标准。

仅当您将 seq-cst 操作与对同一原子变量的获取-释放操作混合使用时,此更改才会影响您(即,如果获取操作从 seq-cst 存储中读取值,或者 seq-cst 操作从发布中读取值)店铺)。

如果您不以这种方式混合操作,那么您不会受到影响(即可以将simply happens beforestrongly happens before视为等效)。

更改的要点是seq-cst 操作和相应的获取/释放操作之间的同步不再影响该特定 seq-cst 操作在全局 seq-cst order 中的位置,但同步本身仍然存在。

这使得此类 seq-cst 操作的 seq-cst 顺序非常没有意义,请参见下文。


该提案提供了以下示例,我将尝试解释我对此的理解:

atomic_int x = 0, y = 0;
int a = 0, b = 0, c = 0;
// Thread 1
x.store(1, seq_cst);
y.store(1, release);
// Thread 2
b = y.fetch_add(1, seq_cst); // b = 1 (the value of y before increment)
c = y.load(relaxed); // c = 3
// Thread 3
y.store(3, seq_cst);
a = x.load(seq_cst); // a = 0
Run Code Online (Sandbox Code Playgroud)

这些注释指出了该代码可以执行的方式之一,标准曾经禁止(在此更改之前),但实际上可能会在受影响的体系结构上发生。

执行过程如下:

atomic_int x = 0, y = 0;
int a = 0, b = 0, c = 0;
// Thread 1
x.store(1, seq_cst);
y.store(1, release);
// Thread 2
b = y.fetch_add(1, seq_cst); // b = 1 (the value of y before increment)
c = y.load(relaxed); // c = 3
// Thread 3
y.store(3, seq_cst);
a = x.load(seq_cst); // a = 0
Run Code Online (Sandbox Code Playgroud)

在哪里:

  • 右侧带括号的数字显示全局 seq-cst 顺序。

  • 左侧的箭头显示了值如何在某些加载和存储之间传播。

  • 中间的箭头显示:

    • 'sequenced before',良好的旧单线程评估顺序。
    • “同步于”,释放-获取同步(seq-cst 加载算作获取操作,seq-cst 存储算作释放操作)。

    这两者一起构成了“之前发生过”。

  • 右侧的箭头基于中间的箭头,它们显示:

    • 新重新定义的“强烈发生在之前”关系。

    • 'Coherence-ordered before'是本提案中引入的新关系,仅用于定义全局 seq-cst 顺序,并且显然不强制同步(与释放获取操作不同)。

      似乎它包括除了影响全局 seq-cst 顺序的“简单发生在之前”之外的所有内容。在这种情况下,常识是,如果加载没有看到存储写入的值,则加载会先于存储进行。

    全局 seq-cst 顺序与两者一致。

请注意,在这张图片中,之前没有发生任何强烈的事情b = y.fetch_add(1, seq_cst);,因此在全局 seq-cst 顺序中,没有任何东西必须在它之前,因此可以将其向上移动到 seq-cst 顺序的开头,即最终发生的情况就是这种情况,即使它读取了稍后(按此顺序)操作生成的值。