其他线程是否总是以相同的顺序看到对不同线程中不同位置的两次原子写入?

Will two atomic writes to different locations in different threads always be seen in the same order by other threads?

类似于我的previous问题,请考虑此代码

-- Initially --
std::atomic<int> x{0};
std::atomic<int> y{0};

-- Thread 1 --
x.store(1, std::memory_order_release);

-- Thread 2 --
y.store(2, std::memory_order_release);

-- Thread 3 --
int r1 = x.load(std::memory_order_acquire);   // x first
int r2 = y.load(std::memory_order_acquire);

-- Thread 4 --
int r3 = y.load(std::memory_order_acquire);   // y first
int r4 = x.load(std::memory_order_acquire);

在 C++11 内存模型下,奇怪的结果 r1==1, r2==0r3==2, r4==0 可能吗?如果我将所有 std::memory_order_acq_rel 替换为 std::memory_order_relaxed 怎么办?

在 x86 上,这样的结果似乎是被禁止的,请参阅 this SO question 但我问的是一般的 C++11 内存模型。

加分题:

我们都同意,std::memory_order_seq_cst 奇怪的结果 在 C++11 中是不允许的。现在,Herb Sutter 在他著名的 atomic<>-weapons talk @ 42:30 中说 std::memory_order_seq_cst 就像 std::memory_order_acq_rel std::memory_order_acquire-在 std::memory_order_release 写入之前负载可能不会移动。我看不出上面示例中的这个附加约束如何防止奇怪的结果。谁能解释一下?

简短的回答是否定的。该标准没有说它们必须是,因此它们不必是。无论您能否想象出具体的实现方式都无关紧要。

Is the weird outcome r1==1, r2==0 and r3==0, r4==2 possible in this case under the C++11 memory model?

是的。 C++ 内存模型允许这样的奇怪的结果

What if I were to replace all std::memory_order_acq_rel by std::memory_order_relaxed?

如果您将所有 memory_order_acquirememory_order_release 替换为 memory_order_relaxed,您的代码没有任何变化。

std::memory_order_seq_cst is just like std::memory_order_acq_rel but std::memory_order_acquire-loads may not move before std::memory_order_release-writes. I cannot see how this additional constraint in the above example would prevent the weird outcome.

"acquire-loads 可能不会在 release-writes 之前移动。"显示顺序一致性约束的一个方面 (memory_order_seq_cst)。

在 C++ 内存模型中,它只保证 seq_cst 具有 acq_rel 语义和 all seq_cst 原子访问具有一些 "total order" 不多也不少。当存在这样的 "total order" 时,我们无法得到 奇怪的结果 因为所有 seq_cst 原子访问都在单线程上以任何交错顺序执行。

你的previous question对待"coherency" of single atomic variable,这个问题问"consistency" of all 原子变量。 C++ 内存模型保证 直观 单个原子变量的一致性,即使是最弱的排序 (relaxed),并且 "sequential consistency" 对于不同的原子变量,只要默认排序 (seq_cst). 当您使用明确的非 seq_cst 排序原子访问时,正如您指出的那样,结果可能很奇怪。

问题中更新的1 代码(在线程 4 中交换了负载 xy)确实测试了所有线程是否同意全球商店订单。

在 C++11 内存模型下,结果 r1==1, r2==0, r3==2, r4==0 是允许的,而且实际上可以在 POWER 上观察到。

在 x86 上不可能出现这种结果,因为 "stores are seen in a consistent order by other processors"。这种结果在连续一致的执行中也是不允许的。


脚注 1:这个问题最初有两个 reader 读 x 然后 y顺序一致的执行是:

-- Initially --
std::atomic<int> x{0};
std::atomic<int> y{0};

-- Thread 4 --
int r3 = x.load(std::memory_order_acquire);

-- Thread 1 --
x.store(1, std::memory_order_release);

-- Thread 3 --
int r1 = x.load(std::memory_order_acquire);
int r2 = y.load(std::memory_order_acquire);

-- Thread 2 --
y.store(2, std::memory_order_release);

-- Thread 4 --
int r4 = y.load(std::memory_order_acquire);

这导致 r1==1, r2==0, r3==0, r4==2。因此,这根本不是一个奇怪的结果。

为了能够说每个 reader 看到了不同的商店订单,我们需要他们以相反的顺序阅读以排除最后一家商店只是被延迟的可能性。

这种重新排序测试称为 IRIW(独立读者、独立作者),我们在其中检查两个读者是否可以看到同一对商店以不同的顺序出现。相关,可能重复:


非常弱的 C++11 内存模型不需要所有线程就存储的全局顺序达成一致,正如@MWid 的回答所说。

此答案将解释一种可能导致线程对存储的全局顺序不一致的可能硬件机制, 这在为无锁代码设置测试时可能是相关的。只是因为如果你喜欢 cpu-architecture1.

就很有趣

有关这些 ISA 的抽象模型,请参阅 A Tutorial Introduction to the ARM and POWER Relaxed Memory Models:ARM 和 POWER 都不能保证所有线程都看到一致的全局存储顺序。 实际观察到这一点在 POWER 芯片上的实践中是可能的,理论上在 ARM 上可能是可能的,但在任何实际实现中可能都不可能。

其他弱排序的 ISA like Alpha 也允许这种重新排序,我认为。ARM 曾经在纸上允许它,但可能没有真正的实现做到这一点重新排序。ARMv8 甚至加强了他们的纸上模型,即使对于未来的硬件也不允许这样做。)

在计算机科学中,存储对所有其他线程同时可见(因此存在单一的全局存储顺序)的机器的术语是“多副本原子" 或 "multi-copy atomic"。 x86 和 SPARC 的 TSO 内存模型有 属性,但 ARM 和 POWER 不需要它。


当前的 SMP 机器使用 MESI 来维护一个单一的连贯缓存域,以便所有内核都具有相同的内存视图。当存储从存储缓冲区提交到 L1d 缓存时,存储变得全局可见。届时,来自 any 其他核心的负载将看到该存储。 所有存储提交到缓存的单一顺序,因为 MESI 维护一个单一的一致性域。有了足够的障碍来阻止局部重新排序,就可以恢复顺序一致性。

商店可以对某些但不是所有其他核心可见它变得全局可见.

POWER CPU 在一个物理核心上使用 Simultaneous MultiThreading (SMT)(超线程的通用术语)到 运行 多个逻辑核心。我们关心的内存排序规则适用于线程 运行 的 逻辑 核心,而不是 物理 核心。

我们通常认为加载是从 L1d 中获取它们的值,但是当从同一核心重新加载最近的存储并且数据直接从存储缓冲区转发时,情况并非如此 . (存储到负载转发,或 SLF)。负载甚至有可能获得一个在 L1d 中从未出现过且永远不会出现的值,即使是在具有部分 SLF 的强顺序 x86 上也是如此。 (请参阅我在 上的回答)。

存储缓冲区在存储指令退役之前跟踪推测性存储,但也在非推测性存储从核心的乱序执行部分(ROB / ReOrder 缓冲区)退役后缓冲非推测性存储。

同一物理核心上的逻辑核心共享一个存储缓冲区。推测性(尚未退休)存储必须对每个逻辑核心保持私有。 (否则,这会将它们的推测结合在一起,并且如果检测到错误推测,则需要两者都回滚。这将破坏 SMT 的部分目的,即在一个线程停滞或从分支错误预测中恢复时保持核心忙碌) .

但是我们可以让其他逻辑内核窥探非推测存储的存储缓冲区,最终肯定会提交给 L1d 缓存。在它们出现之前,其他物理内核上的线程看不到它们,但共享同一物理内核的逻辑内核可以。

(我不确定这是否正是允许这种奇怪的 POWER 的硬件机制,但它是合理的)。

此机制使存储在 SMT 兄弟核心可见之前全局对所有核心可见。但它仍然在内核中是本地的,因此可以通过仅影响存储缓冲区的障碍廉价地避免这种重新排序,而无需实际强制内核之间进行任何缓存交互。

(ARM/POWER 论文中提出的抽象内存模型将其建模为每个内核都有自己的内存缓存视图,缓存之间有链接让它们同步。但在典型的物理现代硬件中,我认为唯一的机制是在 SMT 兄弟之间,而不是在单独的内核之间。)


请注意,x86 根本不允许其他逻辑核心窥探存储缓冲区,因为这会违反 x86 的 TSO 内存模型(通过允许这种奇怪的重新排序)。正如我在 上的回答所解释的那样,具有 SMT(英特尔称为超线程)的英特尔 CPU 在逻辑核心之间静态分区存储缓冲区。


脚注 1:C++ 的抽象模型,或特定 ISA 上的 asm,是您真正需要知道的关于内存排序的所有信息。

没有必要了解硬件细节(并且可能会让您陷入认为某事不可能的陷阱,因为您无法想象它的机制)。