MESI 协议 & std::atomic - 它是否确保所有写入立即对其他线程可见?

MESI Protocol & std::atomic - Does it ensure all writes are immediately visible to other threads?

关于 std::atomic,C++11 标准规定存储到原子变量将对 "reasonable amount of time" 中该变量的加载可见。

来自 29.3p13:

Implementations should make atomic stores visible to atomic loads within a reasonable amount of time.

但是我很想知道在处理基于 MESI 缓存一致性协议(x86、x86-64、ARM 等)的特定 CPU 架构时实际发生了什么。

如果我对 MESI 协议的理解是正确的,一个内核总是会立即读取另一个内核先前 written/being 写入的值,可能是通过窥探它。 (因为写一个值意味着发出一个 RFO 请求,这反过来又使其他缓存行无效)

这是否意味着当一个线程 A 将一个值存储到一个 std::atomic 中时,另一个线程 B 在该原子上连续加载,实际上总是会观察到 A 在 MESI 架构上写入的新值? (假设没有其他线程正在对该原子进行操作)

我所说的“连续”是指在线程 A 发出原子存储之后。 (修改顺序已更新)

From 29.3p13:

Implementations should make atomic stores visible to atomic loads within a reasonable amount of time.

C 和 C++ 标准在线程上随处可见,因此不能用作正式规范。他们使用时间的概念,并在某种程度上暗示一切都按顺序逐步运行(如果不是这样,您将不会有合理的程序语义)然后说某些构造可以无序地看到效果,而不知道哪个是

当看到效果乱序时,线程时间定义错误,因为您没有也会乱序的计时表:您不会在乱序执行动作的情况下进行运动!

甚至 "out of order" 表明有些事情是纯粹顺序的,而其他一些操作可以 "out of order" 相对于第一个。 std::atomic 不是这样定义的。

标准试图说明的是,每个线程都有一个进度概念,具有 CPU 时间或成本指数,并且随着完成的事情越来越多,事情也只能是通过实施稍微重新排序:现在重新排序定义明确,不是根据其他顺序指令,而是根据 cost/cycles/CPU 时间。

因此如果两条指令在线程内顺序执行中彼此接近,它们也会在 CPU 时间内接近。 合理的编译器不应将易失性操作、文件输出或原子操作移到非常昂贵的 "pure" 计算(没有外部可见副作用的计算)之后。

遗憾的是,许多委员会成员甚至无法拼写出一个基本概念!

我将回答真实 CPUs 的真实实现中发生的情况,因为仅基于标准的答案几乎无法说明时间或“即时性”的任何有用信息。

MESI 只是 ISO C++ 没有任何可说的实现细节。 ISO C++ 提供的保证只涉及顺序,而不是实际的时间。 ISO C++ 有意是非特定的,以避免假设它将在“正常”CPU 上执行。在需要显式刷新存储可见性的非连贯机器上的实现在理论上可能是可行的(尽管对于发布/获取和 seq-cst 操作的性能可能很糟糕)

C++ 在时序方面不够具体,甚至允许在单核协作多任务系统(无抢占)上实现,编译器偶尔会插入自愿让步。 (没有任何易失性访问的无限循环或 I/O 是 UB)。 在实际上一次只能执行一个线程的系统上使用 C++ 是完全可行的, 假设您认为调度程序时间片仍然是“合理”的时间量。 (或更少,如果你让步或以其他方式阻止。)

即使是 ISO C++ 用来保证其顺序的形式主义模型也与硬件 ISA 定义其内存模型的方式有很大不同。 C++ 正式保证纯粹是在发生之前和同步方面,而不是“重新”排序石蕊测试或任何类似的东西。例如 无法回答纯 ISO C++ 形式主义。该问答中的“选项 C”用于显示 C++ 保证的弱点;根据 C++ 形式,存储然后加载两个不同 SC 变量的情况不足以暗示基于它发生之前发生,即使必须有所有 SC 操作的总顺序。但在现实生活中,在具有一致缓存和仅本地(在每个 CPU 核心内)内存重新排序的系统上就足够了,即使是在 SC 存储之后立即加载 SC 的 AArch64 本质上仍然给我们一个 StoreLoad 障碍。

when a thread A stores a value into an std::atomic

看你说的“做”店是什么意思

如果你的意思是从存储缓冲区提交到 L1d 缓存,那么是的,那是存储变得全局可见的时刻,在使用 MESI 为所有 CPU 核心提供连贯视图的普通机器上内存。

虽然请注意,在某些 ISA 上,一些其他线程在它们变得全局可见之前被允许查看存储通过缓存。 (即硬件内存模型可能不是“多副本原子”,并允许 IRIW 重新排序。POWER 是我所知道的唯一在现实生活中这样做的例子。有关硬件机制的详细信息,请参阅 :Store在 SMT 线程之间转发已退役的分级存储。)


如果你的意思是在本地执行所以稍后加载在这个线程可以看到它,那么没有。 std::atomic可以使用memory_order 弱于 seq_cst.

所有主流 ISA 的内存排序规则都很弱,足以允许存储缓冲区将指令执行与提交到缓存分离。这也允许推测性乱序执行,方法是在执行后,在我们确定它们在正确的执行路径上之前,将存储放在某个私有的地方。 (直到存储指令从后端的乱序部分退出后,存储才能提交到 L1d,因此已知是非推测性的。)

如果您想在进行任何后续加载之前等待您的商店对其他线程可见,请使用 atomic_thread_fence(memory_order_seq_cst);。 (在标准选择 C++ 的“普通”ISA 上 -> asm 映射将编译为完全障碍)。

大多数 ISA 上,seq_cst 存储(默认)也将停止此线程中的所有后续加载(和存储),直到存储全局可见。但是在 AArch64 上,STLR 是一个顺序释放存储,稍后 loads/stores 的执行不必停止,除非/直到 LDAR(获取加载)即将执行,而 STLR 仍在存储缓冲区中。这尽可能弱地实现了 SC 语义,假设 AArch64 硬件确实以这种方式工作,而不是仅仅将其视为存储 + 完整屏障。

但请注意,只有稍后 loads/stores 才需要阻塞;寄存器上 ALU 指令的乱序执行仍然可以继续。但是,如果您期望由于 FP 操作的依赖链而产生某种定时效果,例如,这不是您可以在 C++ 中依赖的东西。


即使您确实使用了 seq_cst,因此在商店对其他人可见之前此线程中什么也没有发生,这仍然不是即时的。真实硬件上的内核间延迟可以是 maybe 40ns on mainstream modern Intel x86, for example. (This thread doesn't have to stall that long on a memory barrier instruction; some of that time is the cache miss on the other thread trying to read the line that was invalidated by this core's RFO to get exclusive ownership.) Or of course much cheaper for logical cores that share the L1d cache of a physical core:

的数量级