这个话题有很多微妙之处,有很多信息需要筛选。我找不到专门针对这个问题的现有问题/答案,所以在这里。
如果我有一个类型为std::atomic_int
的原子变量 M,其中
- 线程 1 执行
M.store(1, memory_order_release)
-
稍后,线程 2 执行
M.store(2, memory_order_release)
-
甚至后来,线程 3
M.load(memory_order_acquire)
是否有任何合法的情况,其中线程 3 可以读取值1
而不是2
?
我的假设是这是不可能的,因为写-写的连贯性和发生之前的属性。但是,在花了一个小时回顾了C++标准和cpp偏好之后,我仍然无法对这个问题形成简洁明了的答案。
我很想在这里得到一个可靠的参考答案。提前谢谢。
"以后"的概念不是C++标准定义或使用的概念,所以从字面上看,这不是一个可以参考标准来回答的问题。
如果我们改用[intro.races p10]中定义的"发生之前">的概念,那么您问题的答案是肯定的。 (我在这里使用 C++20 最终草案 N4860)。
M
有一个修改顺序[intro.races p4],这是一个总订单。 通过写写一致性 [intro.races p15],如果 #1 发生在 #2之前,则存储值的副作用1
先于值的存储2
,修改顺序为M
。
然后通过写读连贯性 [intro.races p17],如果 #2 发生在 #3 之前,那么 #3 必须从 #2 中获取其值,或者从 #2 之后的某个副作用中获取其值,修改顺序为M
。 但是假设程序不包含其他要M
的存储,在修改顺序中没有遵循#2的其他副作用,因此#3实际上必须从#2中获取其值,也就是说#3必须加载值2
。 特别是 #3 不能从 #1 取值,因为 #1 在M
的修改顺序中位于 #2 之前,而不是相反。 (修改顺序定义为总顺序,这意味着它不能包含循环,因此 #1 和 #2 不可能同时在彼此之前。
请注意,您附加到操作 #1、#2、#3 的获取和发布顺序在此分析中完全无关紧要,如果将它们relaxed
,一切都会相同。 内存排序的重要之处在于操作(示例中未显示),这些操作强制执行 #1 发生在 #2 发生在 #3 之前
。例如,假设在线程 1 中,操作 #1 在存储之前(即按程序顺序在存储之前)排序到特定值的其他原子对象Z
(假设true
)。 同样,线程 2 直到从Z
加载并观察到加载的值true
之后才执行存储 #2。 然后,线程 1 中要Z
的存储必须释放(或seq_cst
),并且必须获取线程 2 中Z
的负载(或seq_cst
)。 这样,您可以获得发布存储Z
与获取负载 [atomics.order p2] 同步,并通过追逐 [intro.races p9-10],您得出结论 #1 发生在 #2 之前。 但同样,对M
的访问不需要特别的排序。
事实上,如果你在排序之前发生了,那么即使M
根本不是原子的,而只是一个普通的int
,而#1、#2、#3只是非原子的普通写入和读取M
。 然后,在发生之前确保这些对M
的访问不会导致 [intro.races p21] 意义上的数据争用,因此程序的行为是明确定义的。 然后我们可以参考[intro.races p13],即"不明显重新排序"的规则。 确实,#2 发生在 #3 之前,并且没有其他副作用 X 对M
因此 #2 发生在 X 之前,X 发生在 #3 之前。 (特别是,对于X = #1来说,这不可能是真的,因为#1发生在#2之前,因此不是相反;通过[intro.races p10],"发生在之前"关系不能包含循环。 因此,#3 必须确定 #2 存储的值,即值2
。
不过,无论哪种方式,魔鬼都在细节中。 如果您实际上在程序中编写了此内容,则分析的重要部分将是验证,无论您的程序如何确保 #2 "晚于"#1,它实际上都会在排序之前施加一个发生。 幼稚或直觉的"以后"概念不一定足够。 例如,检查访问 #1、#2、#3 周围的系统时间是不够的。 也不是像上面的例子那样,额外的标志Z
只能在一个或两个地方通过relaxed
排序来访问,或者更糟的是,如果它根本不是原子的。
如果您不能通过其他操作保证 #1 发生在 #2 发生在 #3 之前,那么绝对不能保证 #3 加载值2
,即使您将所有 #1、#2、#3 都seq_cst
正如几乎每个人都在评论中注意到的那样,如果"稍后"是内存模型的某些外部元素,例如流逝的时间或类似的东西,则无法保证。
但是,如果"后来"实际上是颠倒的"发生在之前",那么这个问题基本上没有任何意义,因为"发生在之前"(对于这种情况)归结为"同步"。"同步"定义为:
如果线程 A 中的原子存储是释放操作,则来自同一变量的线程 B 中的原子加载是获取操作,并且线程 B 中的加载读取线程 A 中的存储写入的值,则线程 A 中的存储与线程 B 中的加载同步。
换句话说,为了使 2 发生在 3 之前,我们应该在线程 3 中具有以下代码(或与其他原子等效的代码):
while(M.load(memory_order_acquire) != 2);
// At this point 2 happens before 3
那么当然,在没有任何其他存储到这个原子的情况下,它将具有值 2。但是,不能保证线程 3 不会在值 2 之前读取值 1while
因为此时它尚未与线程 2 "同步"。
正如您已经建议的那样,如果 1 发生在 2 之前,那么由于"写写一致性",我们在获得值 1 后永远不会看到值 2。