【问题标题】:What is the significance of 'strongly happens before' compared to '(simply) happens before'?与“(简单)之前发生”相比,“强烈发生在之前”有什么意义?
【发布时间】:2022-01-02 07:27:06
【问题描述】:

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

[intro.races]

11 评估 A 只发生在评估 B 之前

(11.1) — A 在 B 之前排序,或者
(11.2) — A 与 B 同步,或者
(11.3) — A 简单地发生在 X 之前,X 简单地发生在 B 之前。

[注10:在没有消费操作的情况下,发生在之前和简单地发生在之前的关系是相同的。 ——尾注]

12 评估 A 强烈发生在评估 D 之前

(12.1) — A 在 D 之前排序,或者
(12.2) — A 与 D 同步,A 和 D 都是顺序一致的原子操作 ([atomics.order]),或
(12.3) — 存在评估 B 和 C,使得 A 在 B 之前排序,B 只是在 C 之前发生,C 在 D 之前排序,或者
(12.4) - 有一个求值 B 使得 A 强烈地发生在 B 之前,而 B 强烈地发生在 D 之前。

[注 11:非正式地,如果 A 强烈地发生在 B 之前,那么 A 似乎在所有情况下都在 B 之前被评估。强烈发生在排除消费操作之前。 ——尾注]

(我的粗体字)

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

为什么这种差异很重要?

“强烈发生在之前”是在 C++20 中引入的,在 C++20 之前,“简单地发生在之前”曾经被称为“强烈发生在之前”。为什么要介绍它?

[atomics.order]/4 表示所有 seq-cst 操作的总顺序与 'strongly occurred before' 一致。

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


我忽略了简单的“发生在之前”,因为它与“仅发生在之前”的区别仅在于它对 memory_order_consume 的处理,使用 temporarily discouraged,因为显然大多数(全部?)主要编译器将其视为memory_order_acquire

我已经看过this Q&A,但它没有解释为什么存在“强烈发生之前”,也没有完全说明它的含义(它只是声明它不尊重发布-获取同步,并非完全如此)。


发现 the proposal 介绍了“只是发生在之前”。

我不完全理解,但解释如下:

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

【问题讨论】:

    标签: c++ multithreading concurrency language-lawyer c++20


    【解决方案1】:

    这是我目前的理解,可能不完整或不正确。验证将不胜感激。


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

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

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


    这项更改是在提案 P0668R5: Revising the C++ memory model 中引入的,该提案基于 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 顺序中的位置,但同步本身仍然存在。

    这使得此类 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
    

    cmets 表示此代码可以执行的一种方式,标准曾经禁止(在此更改之前),但实际上可能发生在受影响的架构上。

    执行如下:

    .-- T3 y.store(3, seq_cst);                   --.                 (2)
    |        |                                      | strongly
    |        | sequenced before                     | happens
    |        V                                      | before
    |   T3 a = x.load(seq_cst); // a = 0    --.   <-'                 (3)
    |                                         : coherence-
    |                                         : ordered
    |                                         : before
    |   T1 x.store(1, seq_cst);             <-'   --. --.             (4)
    |        |                                      |st |
    |        | sequenced before                     |h  |
    |        V                                      |b  |
    | . T1 y.store(1, release);                   <-'   |
    | |      :                                          | strongly
    | |      : synchronizes with                        | happens
    | |      V                                          | before
    | > T2 b = y.fetch_add(1, seq_cst); // b = 1  --.   |             (1)
    |        |                                      |st |
    |        | sequenced before                     |h  |
    |        V                                      |b  |
    '-> T2 c = y.load(relaxed); // c = 3          <-' <-'
    

    地点:

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

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

    • 中间的箭头显示:

      • 'Sequenced before',古老的单线程评估顺序。
      • 'Synchronizes with',释放-获取同步(seq-cst 加载计为获取操作,seq-cst 存储计为释放操作)。

      这两者共同构成“只是发生在之前”。

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

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

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

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

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

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

    【讨论】:

      猜你喜欢
      • 2020-03-18
      • 2020-08-09
      • 1970-01-01
      • 2023-03-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多