9

与我之前的问题类似,请考虑此代码

-- 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 内存模型下会出现奇怪的结果 吗?如果我要替换 all怎么办?r1==1, r2==0r3==2, r4==0std::memory_order_acq_relstd::memory_order_relaxed

在 x86 上,这样的结果似乎是被禁止的,请参阅这个 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 -loads std::memory_order_acquire可能不会在std::memory_order_release-writes 之前移动。我看不出上面示例中的这个附加约束如何防止奇怪的结果。谁能解释一下?

4

4 回答 4

8

这种重新排序测试称为 IRIW(Independent Readers,Independent Writers),我们在其中检查两个阅读器是否可以看到同一对商店以不同的顺序出现。相关的,可能是重复的:Acquire/release semantics with 4 threads


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

该答案将解释一种可能的硬件机制,该机制可能导致线程对存储的全局顺序存在分歧,这在为无锁代码设置测试时可能相关。只是因为如果你喜欢 cpu-architecture 1会很有趣。

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

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

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


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

在全局可见之前,商店可以对某些但不是所有其他核心可见

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

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

存储缓冲区在存储指令退出之前跟踪推测存储,但也在它们从内核的无序执行部分(ROB / ReOrder 缓冲区)退出后缓冲非推测存储。

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

但是我们可以让其他逻辑核心窥探存储缓冲区的非推测性存储,这些存储最终肯定会提交到 L1d 缓存。在他们这样做之前,其他物理内核上的线程无法看到它们,但共享相同物理内核的逻辑内核可以。

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

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

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


请注意,x86 根本不允许其他逻辑内核窥探存储缓冲区,因为这会违反 x86 的 TSO 内存模型(通过允许这种奇怪的重新排序)。作为我对在一个带有 HT 的核心上执行的线程之间的数据交换将使用什么的回答?解释说,带有 SMT(英特尔称之为超线程)的英特尔 CPU 在逻辑内核之间静态划分存储缓冲区。


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

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

于 2018-06-04T11:11:09.353 回答
7

问题中更新的1代码(在线程 4 中加载xy 交换)确实测试了所有线程是否同意全局存储顺序。

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

在 x86 上,这种结果是不可能的,因为“其他处理器以一致的顺序看到存储”。在顺序一致的执行中也不允许此结果。


脚注 1 : 这个问题最初让两位读者都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. 因此,这根本不是一个奇怪的结果。

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

于 2015-01-08T18:19:17.763 回答
4

最简洁的答案是不。标准并没有说它们必须是,因此它们不必是。无论您能否想象出一种特定的方式来实现这一点,都无关紧要。

于 2015-01-06T21:28:39.233 回答
1

在这种情况下,在 C++11 内存模型下会出现奇怪的结果 吗?r1==1, r2==0r3==0, r4==2

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

如果我要替换 allstd::memory_order_acq_rel怎么std::memory_order_relaxed办?

如果你替换 allmemory_order_acquirememory_order_releaseby memory_order_relaxed,你的代码没有任何改变。

std::memory_order_seq_cst就像std::memory_order_acq_rel但是std::memory_order_acquire-loads 可能不会在std::memory_order_release-writes 之前移动。我看不出上面示例中的这个附加约束如何防止奇怪的结果

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

在 C++ 内存模型中,它只保证seq_cst具有acq_rel语义,并且所有 seq_cst原子访问都具有某种“全序”,不多也不少。当存在这样的“全顺序”时,我们不会得到奇怪的结果,因为所有seq_cst原子访问都像在单个线程上以任何交错顺序一样执行。

之前的问题处理单个原子变量的“一致性”,而这个问题询问所有原子变量的“一致性”。C++ 内存模型保证了单个原子变量的直观一致性,即使是最弱的排序(relaxed),以及不同原子变量的“顺序一致性”,只要默认排序(seq_cst)。当您使用明确seq_cst的无序原子访问时,正如您所指出的那样,结果可能很奇怪。

于 2015-01-07T06:31:56.237 回答