这个内存顺序正确吗?

And*_*hko 3 c++ multithreading atomic memory-barriers stdatomic

std::atomic<bool> x{ false };
std::atomic<bool> y{ false };

// thread 1
y.store(true, std::memory_order_seq_cst);
x.store(true, std::memory_order_release);

// thread2
while (!x.load(std::memory_order_relaxed);
assert(y.load(std::memory_order_seq_cst)); // !!!
Run Code Online (Sandbox Code Playgroud)

断言会失败吗?我的理解是:虽然读取x是“放松的”,但一旦“线程2”看到“线程1”的写入,它就看不到yfalse因为写入y发生在写入之前x

内存顺序是从现实生活中的案例复制的,对于这个示例来说可能会更弱,但我没有改变它,以免错过任何微妙之处。

Pet*_*des 9

是的,ISO C++ 允许取ay.load之前的值,因为不是或更强。x.loadtruex.loadacquire

y.store(更正式地说,它不会在放在before的作者和读者之前创建发生之前y.load

在许多 ISA(例如 x86 或 PowerPC)上,实际上不可能观察到它,x86 是因为一般不允许 LoadLoad 重新排序,而 PowerPC 是因为 seq_cst 加载在指令之前涉及一些防护(可能会阻止 IRIW) reordering),最终足以阻止 LoadLoad 重新排序,即使是早期宽松的负载。https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html显示了 PowerPC 的load(seq_cst)存在 hwsync; ld; cmp; bc; isynchwsync是一个完整的屏障,与他们使用的指令相同atomic_thread_fence(seq_cst)

但是您也许能够在 AArch64 上观察到这一点,其中 LDR (load(relaxed)可以使用稍后的 LDAR ( ) 重新排序load(seq_cst)。要实际看到它,您可能需要单独的缓存行中的变量,并且要正确预测循环退出分支,否则稍后的 SC 加载直到自旋循环加载产生一个值之后才会进入管道(并且分支错误预测恢复已重新引导前端从正确的路径获取)。


Nat*_*dge 6

彼得的回答是正确的。我只是想解决这个问题背后可能存在的误解。

您必须避免过多地解读“发生在之前”关系的名称。形式上,HB只是给定一个关系的任意名称,该关系在任何给定操作与另一个操作之间可能成立,也可能不成立,遵循一定的规则并产生一定的后果。它不一定对应于机器内部实际发生的事件。

将其视为数学定义,因为它就是这样。您只能根据其精确的形式定义进行推理,而不能根据英语单词“发生”和“之前”的日常含义进行推理。

听起来您将“A 发生在 B 之前”解释为“在所有情况下 A 将在 B 之前被观察到”,但其含义远比这有限。确实y.store()发生在 之前x.store(),并且您可以从 C++ 标准中给出的定义来证明这一点,因为它们在特定线程中按该顺序排序(换句话说,“发生在”之前始终与程序顺序一致)。但这并不意味着断言不会失败。

为了从关于 HB 关系的事实中得出有关程序实际行为的推论,您必须利用标准 [intro.races p13-18] 中的一致性规则,而这些规则只能让您得出关于以下两个关系的结论:一个对象。它们可以概括为:如果A和B是对同一个对象的操作,并且A发生在B之前,那么B将观察A的结果。在这种情况下,A和B相对于彼此的行为是“什么”你认为它应该是这样”,就好像它们在 20 世纪 80 年代的一台严格有序的机器上按程序顺序执行一样。

因此,为了证明断言不会失败,您需要证明y.store()之前没有发生过这种情况x.store();相反,您需要证明这种情况y.store()之前发生过y.load()。而那,你将无法做到。

粗略地说,链接两个线程之间的 HB 关系的唯一方法是通过同步,这通常是由于获取负载观察释放存储的结果而产生的。您确实知道x.load()循环中的最后一个必须观察 的结果x.store(),但由于负载不是获取的,所以它对我们没有帮助。
的操作y都是seq_cst,所以特别是存储和加载分别是释放和获取,但为了从中得出任何结论,我们必须知道 观察y.load()的结果y.store(),这正是我们不知道的并且是试图找出答案。

关于同步的部分给出了有关修复的提示:负载x应升级为获取或更强。或者,您可以将其保留为宽松状态,并在循环和 之间放置一个获取栅栏,这大致具有追溯升级最终获取的y.load()效果。x.load()