与(简单地)发生在';之前相比,';强烈发生在';之前有什么意义呢?
该标准定义了几个"在此之前发生"关系,这些关系将良好的旧"在此之前排序"扩展到多个线程:
[intro.races]
11评估A仅发生在评估B之前,如果出现以下情况
(11.1)-A排在B之前,或
(11.2)-A与B同步,或
(11.3)-A在X之前,X在B之前。[注10:在没有使用操作的情况下,在关系相同之前和简单地在关系相同之前发生。-结束语]
12评估A强烈发生在评估D If之前,
(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操作的总顺序与‘强烈发生在此之前’一致。
这是否意味着它与"以前简单发生的事情"不一致?如果是,原因何在?
我忽略了简单的"发生在此之前",因为它与"简单发生在此之前"的不同之处只在于它对
memory_order_consume
的处理,它的用法是temporarily discouraged,因为显然大多数(all?)主要编译器将其视为memory_order_acquire
。
我已经看过this Q&A,但它没有解释为什么‘强烈发生在’存在之前‘,也没有完全说明它的含义(它只是声明它不尊重释放-获取同步,事实并非如此)。
发现the proposal引入的"仅发生在此之前"。
我不完全理解,但解释如下:
- "之前强烈发生"是"简单发生在之前"的弱化版本。
- 只有当seq-cst与aqc-rel在同一变量上混合使用时(我认为,这意味着获取加载从seq-cst存储中读取值,或者seq-cst加载从释放存储中读取值),才能观察到差异。但对我来说,将两者混合的确切效果还不清楚。
解决方案
这是我目前的理解,可能不完整或不正确。如能核实,将不胜感激。
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 before
和strongly 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
注释指示此代码可以执行的一种方式,这是标准过去禁止的(在此更改之前),但实际上可能会发生在受影响的体系结构上。
执行过程如下:
.-- 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 <-' <-'
其中:
右侧的括号数字显示全局序号-CST顺序。
左侧的箭头显示值如何在某些加载和存储之间传播。
中间的箭头显示:
- 'Sequenced before',很老的单线程求值顺序。
- ‘同步于’,释放-获取同步(seq-cst加载计数为获取操作,seq-cst存储计数为释放操作)。
这两个加在一起构成"简单地发生在此之前"。
右侧的箭头基于中间的箭头,它们显示:
新定义的"强发生在此之前"关系。
'Coherence-ordered before',本方案中引入的新关系,仅用于定义全局seq-cst顺序,显然不强制同步(不同于Release-Acquisition操作)。
它似乎包括了影响全球SEQ-CST秩序的所有事情,而不是"之前发生的事情"。在本例中,如果加载看不到存储写入的值,则加载将先于存储进行,这是常识。
全局序号-CST顺序与两者一致。
请注意,在此图中,b = y.fetch_add(1, seq_cst);
之前没有强烈的事件发生,因此在全局seq-cst顺序中没有必须在其之前的任何内容,因此可以将其向上移动到seq-cst顺序的开始,这就是最终发生的情况,即使它读取后面(按此顺序)操作生成的值。
相关文章