内存屏障是否强制缓存一致性?

10
我正在阅读这个关于使用bool进行线程控制的问题,被@eran所回答的内容所吸引:

只有在单核处理器上,所有线程都使用相同的缓存时,使用volatile就足够了。在多核处理器上,如果一个核心上调用stop(),而另一个核心上执行run(),那么CPU缓存同步可能需要一段时间,这意味着两个核心可能看到isRunning_的两个不同视图。

如果使用同步机制,它们将确保所有缓存获取相同的值,但代价是程序会暂停一段时间。性能和正确性哪个更重要取决于您的实际需求。

我花了一个小时以上搜索某些声明说同步原语会强制执行缓存一致性,但没有成功。我找到的最接近的是维基百科的说明:

关键字volatile不能保证内存屏障强制执行高速缓存一致性。

这表明内存屏障确实强制缓存一致性,由于某些同步原语使用内存屏障进行实现(同样来自维基百科),这是一些“证据”。但我不确定是否应该相信它,也不能确定我是否解释正确。请问有人能够澄清一下吗?

1
没错,在C和C++中使用volatile关键字对线程同步没有任何作用(不确定C#是否有影响)。内存屏障确实可以强制执行缓存一致性。你可能需要了解强/弱内存模型以及内存排序 - Chris O
3个回答

16

简短回答:缓存一致性大部分时间都是有效的,但并非总是如此。您仍然可能读取陈旧的数据。如果您不想冒险,只需使用内存屏障。

详细回答:CPU核心不再直接连接到主存储器。所有负载和存储都必须通过缓存来完成。每个CPU拥有自己的私有缓存会引起新问题。如果多个CPU正在访问相同的内存,则必须确保两个处理器始终看到相同的内存内容。如果一个缓存行在一个处理器上是脏的(即尚未写回到主存储器),并且第二个处理器尝试读取相同的内存位置,则读取操作不能直接转至主存储器。相反,需要第一个处理器的缓存行内容。现在的问题是这个缓存行传输何时发生?这个问题很容易回答:当一个处理器需要另一个处理器缓存中脏的缓存行进行读取或写入时。但是,处理器如何确定另一个处理器的缓存中是否存在脏缓存行呢?仅仅因为另一个处理器加载了缓存行就假定它是脏的是次优的(最好)。通常,大多数内存访问都是读访问,并且生成的缓存行不是脏的。这就是缓存一致性协议的作用。通过MESI或其他缓存一致性协议,CPU通过其缓存维护数据一致性。

在缓存一致性机制的作用下,即使另一个CPU修改了缓存行,我们也应该始终看到最新的值,这是缓存一致性协议的整个目的。通常情况下,当缓存行被修改时,相应的CPU会向所有其他CPU发送“使缓存行无效”的请求。事实证明,CPU可以立即发送确认响应以响应使缓存行无效的请求,但是将实际的缓存行无效化推迟到稍后的时间点。这是通过无效队列完成的。现在,如果我们不幸在此短时间窗口(CPU确认使缓存行无效的请求和实际使缓存行无效之间)中读取缓存行,则可能读取到过期的值。那么为什么CPU要做这样可怕的事情呢?简单的答案是性能。因此,让我们探讨一下无效队列可以提高性能的不同场景。
  • 场景1:CPU1从CPU2接收到一个使缓存行无效的请求。CPU1还有很多存储和加载操作排队等待进行,这意味着对请求的缓存行进行使其无效需要时间,并且CPU2会被阻塞等待确认响应。

  • 场景2:CPU1在短时间内接收到了许多使缓存行无效的请求。现在,CPU1需要时间来使所有的缓存行无效。

将一个条目放入失效队列实际上是CPU的承诺,在传输有关该高速缓存行的任何MESI协议消息之前处理该条目。因此,失效队列是我们在读取单个变量时可能无法看到最新值的原因。
现在敏锐的读者可能会想到,当CPU想要读取高速缓存行时,它可以先扫描失效队列,然后再从缓存中读取。这应该避免问题。然而,CPU和失效队列物理上位于缓存的相对面,这限制了CPU直接访问失效队列。(一个CPU的高速缓存的失效队列是通过系统总线由其他CPU的高速缓存一致性消息填充的。因此,将失效队列放置在缓存和系统总线之间似乎很合理)。因此,为了确实看到任何共享变量的最新值,我们应该清空失效队列。通常,读内存屏障能够完成这个操作。
我刚刚谈论了失效队列和读内存屏障。[1]是一个理解读和写内存屏障以及MESI高速缓存一致性协议细节的好参考资料。 [1]http://www.puppetmastertrading.com/images/hwViewForSwHackers.pdf

尽管存在无效队列,大多数ISA都具有内存模型,保证所有其他核心在两个存储的顺序上达成一致(IRIW litmus测试)。 PowerPC是一个明显的例外,具有实际可以执行此操作的硬件。(ARMv7在纸面上允许此操作,但从未有硬件执行;ARMv8是多副本原子的)。[在不同线程中对不同位置进行两次原子写入,其他线程是否总是以相同的顺序看到它们?] (https://dev59.com/Gl4c5IYBdhLWcg3wia1P#50679223) - Peter Cordes
失效队列是否引入了任何新的重新排序可能性,还是只是让读取核心看起来比其他核心更“领先”于它正在做的事情?我从来没有清楚地知道它们在考虑内存排序时为什么相关。(但我对PowerPC不是很熟悉。)是否有一些试金石,在某些机器上允许最终结果,这在使用存储缓冲区、乱序执行/命中-未命中负载时是不可能的,但不是失效队列?我应该把它作为一个新问题来问。 - Peter Cordes
有人对此进行了后续提问,如果您有兴趣回答:缓存一致性是否总是防止读取过期值?失效队列是否允许? - Peter Cordes

11
据我所知,同步原语不会影响缓存一致性。"Cache"是法语中的"hidden",它不应该对用户可见。缓存一致性协议应该在不需要程序员参与的情况下工作。
同步原语将影响内存顺序,这是通过处理器的ISA明确定义和可见的。
有一份详细信息的好文章可以查阅:《计算机体系结构综合讲座:内存一致性和缓存一致性入门》
编辑:澄清你的疑问
维基百科的说法稍微有些错误。我认为混淆可能来自于术语"memory consistency"和"cache coherency"。它们并不意味着相同的事情。
C中的"volatile"关键字表示变量总是从内存中读取(而不是寄存器),而且编译器不会重新排列加载/存储。这并不意味着硬件不会重新排列加载/存储。这是一个内存一致性问题。当使用较弱的一致性模型时,程序员需要使用同步原语来强制执行特定的排序。这与缓存一致性不同。例如,如果线程1修改位置A,那么在此事件之后,线程2加载位置A时,它将接收到已更新(一致)的值。如果使用了缓存一致性,则应该自动发生。内存顺序是另一个问题。你可以查看著名的论文《共享内存一致性模型入门》以获取更多信息。其中较为知名的例子是Dekker算法,它需要顺序一致性或同步原语。

编辑2: 我想要澄清一件事情。虽然我的缓存一致性的例子是正确的,但存在一种情况,内存一致性似乎可能与它重叠。当处理器中执行存储操作时,但延迟到达缓存时(它们在存储队列/缓冲区中),由于处理器的缓存尚未接收到更新的值,其他缓存也不会接收到。这可能看起来像是一个缓存一致性问题,但实际上它不是,并且实际上是ISA的内存一致性模型的一部分。在这种情况下,同步原语可以用于刷新存储队列到缓存。请记住,您高亮显示的维基百科文本是正确的,但这个文本有点错误:关键字volatile不能保证内存屏障以强制执行缓存一致性。应该说:关键字volatile不能保证内存屏障以强制执行内存一致性


4
我会尝试在EDIT2中澄清这一点,但我理解这可能会让人感到困惑。缓存一致性是一种硬件协议,用户无法控制它。然而,在某些情况下,新值可能会延迟写入高速缓存。在这些情况下,没有任何一个高速缓存会看到新值。在这里,您可以使用同步原语将存储队列刷新到高速缓存中。一旦它在本地高速缓存中,缓存一致性协议将自动使新值对其他缓存可见。你看到区别了吗?需要注意的重要一点是缓存一致性≠内存一致性。 - hayesti
2
因此,如果我们重新表述您的问题“为什么使用同步原语而不是布尔值来强制内存一致性?”,那么我们就会得到一些有趣的东西。总结一下答案,您需要多个变量来进行同步,并且这些变量需要特殊属性才能在单个处理器中进行序列化和刷新。即使如此,在离开关键部分之前,您也需要能够刷新您的临界区域。阅读此文,了解在没有同步原语的情况下在x86机器上运行Dekker算法时遇到的问题。 - hayesti
1
+1 - 这比我四年前的回答更正确。在大多数情况下,一致性而不是连贯性是问题所在,这也是volatile失败的原因。如果可以的话,再加一个+1,引用这两篇论文,这些论文是计算机体系结构界最著名的研究人员撰写的。 - Eran
1
@Wad,你最新的链接很好,语句“同步原语强制所有CPU看到更新的状态”也很好。问题在于你最初问的是它们是否强制“缓存一致性”,而它们并不是。澄清和讨论就是从这里开始的。 - hayesti
1
Wad,我同意hayesti上面的评论。我现在时间有点短,没法阅读其他材料,所以无法对那个链接发表评论。我知道答案中的论文已经有一段时间了,认为它们是很好的资源。连贯性、一致性、内存模型等都是非常复杂的主题,需要认真阅读才能理解。至于@usr的答案,我不知道谁给它投了反对票,也不知道为什么。我只能说,在我看来,haysti的答案更好。 - Eran
显示剩余15条评论

4
维基百科告诉我们,volatile并不意味着会插入内存屏障以确保缓存一致性。然而,适当的内存屏障将确保多个CPU核之间的内存访问是一致的。你可能会发现阅读std::memory_order文档很有帮助。

谢谢。我了解volatile的作用,但我的问题是是否有明确说明“正确的内存屏障将强制执行多个CPU核之间的内存访问一致性”的内容?你能指出任何相关资料吗? - Wad
这也让人感到困惑,因为我所了解的关于缓存同步的内容是它发生在硬件层面 - 那么一个软件的“概念”如何强制执行它呢? - Wad
1
@Wad 一些例子是 CLFLUSHMFENCE IA32 指令,可以在这里找到大量文档 - Chris O
2
@Wad 我指向了您 std::memory_order,它与 std::atomic_thread_fence 一起可以用于在代码中插入内存屏障。由于每个 CPU 架构都有自己的屏障,甚至具有不同的严格要求(例如弱排序 vs 强排序),因此您可以使用这个高级概念,并让编译器为目标 CPU 插入正确的指令。当然,缓存是由硬件实现的,但 ALU 也可以由软件驱动。 - JustSid

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接