我正在阅读this question about using a bool for thread control并被@eran的回答所吸引:
仅在单核上使用volatile就足够了,其中所有线程都使用相同的缓存。在多核上,如果在一个核上调用stop()而在另一个核上执行run(),则CPU缓存可能需要一些时间来进行同步,这意味着两个核可能会看到两个不同的isRunning _视图。
如果您使用同步机制,它们将确保所有缓存获得相同的值,代价是暂停程序一段时间。性能或正确性对您来说更重要取决于您的实际需求。
我花了一个多小时搜索一些声明,说同步原语强制缓存一致但失败了。我最接近的是Wikipedia:
关键字volatile不保证内存屏障可以强制执行缓存一致性。
这表明内存屏障会强制缓存一致性,并且由于某些同步原语是使用内存屏障实现的(同样来自维基百科),这是一些"证据"。
但我不知道是否相信这一点与否,并确保我不会误解它。
有人可以澄清一下吗?
答案 0 :(得分:7)
据我所知,同步原语根本不会影响缓存一致性。 缓存是隐藏的法语,它不应该对用户可见。缓存一致性协议应该在没有程序员参与的情况下工作。
同步原语将影响内存排序,这是通过处理器的ISA明确定义并且对用户可见。
详细信息的好来源是计算机体系结构综合讲座中的A Primer on Memory Consistency and Cache Coherence。
编辑:澄清您的疑问
维基百科的说法有点不对劲。我认为混淆可能来自术语内存一致性和缓存一致性。他们的意思不一样。
C中的volatile
关键字表示始终从内存中读取变量(而不是寄存器),并且编译器不会对其周围的加载/存储重新排序。它并不意味着硬件不会对装载/存储重新排序。这是内存一致性问题。当使用较弱的一致性模型时,程序员需要使用同步原语来强制执行特定的排序。这与缓存一致性不同。例如,如果线程1修改位置A,则在此事件之后线程2加载位置A,它将接收更新的(一致的)值。如果使用缓存一致性,这应该自动发生。内存排序是一个不同的问题。您可以查看着名论文Shared Memory Consistency Models: A Tutorial以获取更多信息。其中一个比较着名的例子是Dekker's Algorithm,它需要顺序一致性或同步原语。
EDIT2 :我想澄清一件事。虽然我的缓存一致性示例是正确的,但是存在内存一致性似乎与其重叠的情况。存储在处理器中执行但延迟到缓存(它们位于存储队列/缓冲区中)时。由于处理器的高速缓存没有收到更新的值,因此其他高速缓存也不会。这可能似乎就像缓存一致性问题,但实际上它并不是,实际上是ISA的内存一致性模型的一部分。在这种情况下,可以使用同步原语将存储队列刷新到缓存。考虑到这一点,以粗体突出显示的维基百科文本是正确的,但另一个仍然有点错误:关键字volatile不保证内存屏障可以强制执行缓存一致性。它应该说:关键字volatile不保证内存屏障可以强制执行内存一致性。
答案 1 :(得分:3)
维基百科告诉你的是volatile
并不意味着将插入内存屏障来强制执行缓存一致性。但是,适当的内存屏障会强制多个CPU内核之间的内存访问是一致的,您可能会发现阅读std::memory_order文档很有帮助。
答案 2 :(得分:1)
简短回答:缓存一致性在大多数情况下有效,但并非总是如此。您仍然可以读取过时的数据。如果您不想冒险,那就使用内存屏障
长答案:CPU内核不再直接连接到主存储器。所有加载和存储都必须经过缓存。每个CPU都有自己的专用缓存这一事实引起了新的问题。如果有多个CPU正在访问同一内存,则仍必须确保两个处理器始终都能看到相同的内存内容。如果一个处理器上的高速缓存行脏了(即尚未将其写回到主存储器),而第二个处理器试图读取相同的存储器位置,则读取操作不能仅发送到主存储器。 。而是需要第一个处理器的缓存行的内容。现在的问题是,何时必须进行此缓存行传输?这个问题很容易回答:当一个处理器需要高速缓存行时,该行在另一处理器的高速缓存中脏了以进行读取或写入。但是,一个处理器如何确定另一个处理器的缓存中的缓存行是否脏了?假设仅由于高速缓存行由另一个处理器加载而已(不是最佳选择)。通常,大多数内存访问是读取访问,并且生成的高速缓存行不会变脏。这是缓存一致性协议。 CPU通过MESI或其他某种缓存一致性协议在整个缓存中保持数据一致性。
在具有缓存一致性的情况下,即使由另一个CPU修改了缓存行的最新值,我们也不应该总是看到它吗?毕竟,这是缓存一致性协议的全部目的。通常,当修改缓存线时,相应的CPU向所有其他CPU发送“无效缓存线”请求。事实证明,CPU可以立即向无效请求发送确认,但将高速缓存行的实际无效推迟到以后的某个时间点。这是通过无效队列完成的。现在,如果我们很不幸在这个短窗口内(在CPU确认无效请求和实际上使高速缓存行无效之间)读取高速缓存行,那么我们可以读取一个过时的值。现在,为什么CPU会做如此可怕的事情。简单的答案就是性能。因此,让我们研究一下失效队列可以提高性能的不同情况
Scenario 1 : CPU1 receives an invalidation request from CPU2. CPU1 also has a lot of stores and loads queued up for the cache. This means that the invalidation of the requested cacheline takes times and CPU2 gets stalled waiting for the acknowledgment
Scenario 2 : CPU1 receives a lot of invalidation requests in a short amount of time. Now it takes time for CPU1 to invalidate all the cachelines.
将条目放入无效队列实质上是CPU承诺在传输有关该高速缓存行的任何MESI协议消息之前处理该条目。因此,无效队列是即使简单读取单个变量也可能看不到最新值的原因。
现在,敏锐的读者可能会想,当CPU要读取高速缓存行时,它可以先扫描无效队列,然后再从高速缓存中读取。这应该避免该问题。但是,CPU和失效队列实际上位于高速缓存的相对侧,这限制了CPU直接访问失效队列。 (一个CPU的缓存的无效队列由来自其他CPU的缓存一致性消息通过系统总线填充。因此,将无效队列放置在缓存和系统总线之间是有意义的)。因此,为了实际看到任何共享变量的最新值,我们应该清空失效队列。通常,读取内存屏障可以做到这一点。
我刚刚谈到了无效队列和读取内存障碍。 [1]为理解读写内存障碍的需求以及MESI缓存一致性协议的细节提供了很好的参考
[1] http://www.puppetmastertrading.com/images/hwViewForSwHackers.pdf