C11 标准的规定如下。
5.1.2.4 多线程执行和数据竞争
评估 A在16)评估 B之前是依赖排序的,如果:
— A 对原子对象 M 执行释放操作,并且在另一个线程中,B 对 M 执行消耗操作并读取由 A 为首的释放序列中的任何副作用写入的值,或
— 对于某些评估 X,A 在 X 之前是依赖排序的,并且 X 携带对 B 的依赖。
如果 A 与 B 同步,则 A线程间评估发生在评估 B 之前,A 在 B 之前是依赖排序的,或者对于某些评估 X:
— A 与 X 同步,并且 X 在 B 之前排序,
— A 在 X 之前排序,并且 X 线程间发生在 B 之前,或者
— 线程间发生在 X 之前,而 X 线程间发生在 B 之前。
注 7 “线程间发生在之前”关系描述了“序列之前”、“同步于”和“依赖顺序之前”关系的任意连接,但有两个例外。第一个例外是不允许串联以''dependency-ordered before'' 后跟''sequenced before'' 结尾。这种限制的原因是,参与“依赖排序之前”关系的消费操作仅针对该消费操作实际携带依赖关系的操作提供排序。此限制仅适用于此类连接的末尾的原因是任何后续发布操作都将为先前的消费操作提供所需的排序。第二个例外是不允许串联完全由''sequenced before''组成。这种限制的原因是(1)允许“线程间发生之前”被传递关闭,(2)下面定义的“发生之前”关系提供了完全由“顺序之前”组成的关系''。
如果 A 在 B 之前排序,或者A 线程间发生在 B之前,则评估 A发生在评估 B 之前。
相对于 M 的值计算 B,对象 M 上的可见副作用 A满足条件:
— 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。这被(17)明确规定为不是ITHB 连接,尽管 (16) 说了什么。
我们知道,由于 A 和 D 不在同一个执行线程中,所以 A 不是 SeqB D;因此 (18) 中对于 HB 的两个条件都不满足,并且 A 不满足 HB D。
因此,A 对 D 不可见,因为不满足 (19) 的条件之一。断言可能会失败。
那么,这将如何发挥作用,在 C++ 标准的内存模型讨论和此处的第 4.2 节控制依赖项中进行了描述:
- (提前一段时间)线程 2 的分支预测器猜测
if
将被采用。
- 线程 2 接近预测采取的分支并开始推测性获取。
- 线程 2 乱序并推测性地
0xGUNK
从y
(评估 D)加载。(也许它还没有从缓存中驱逐?)。
- 线程 1 存储
20
到y
(评估 A)
- 线程 1 存储
10
到x
(评估 B)
10
线程 2从x
(评估 C)加载
- 线程 2 确认
if
被占用。
- 线程 2 的推测性负载
y == 0xGUNK
已提交。
- 线程 2 断言失败。
允许评估 D 在 C 之前重新排序的原因是因为消耗不禁止它。这与acquire-load不同,它可以防止在其之后的任何加载/存储按程序顺序在其之前重新排序。同样,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; }
该工具报告了两种一致的、无竞争的情况,即:
其中y=20
被成功读取,并且
在其中y=30
读取“陈旧”初始化值。手绘圆圈是我的。
相比之下,当mo_acquire
用于加载时,CppMem 只报告一个一致的、无竞争的场景,即正确的场景:
在其中y=20
读取。