38

另一周,我写了一个小线程类和一个单向消息管道来允许线程之间的通信(显然,每个线程有两个管道,用于双向通信)。在我的 Athlon 64 X2 上一切正常,但我想知道如果两个线程都在查看同一个变量并且每个内核上该变量的本地缓存值不同步,我是否会遇到任何问题。

我知道volatile关键字会强制变量从内存中刷新,但是多核 x86 处理器有没有办法强制所有内核的缓存同步?这是我需要担心的事情吗,或者轻量级锁定机制的volatile和正确使用(我使用 _InterlockedExchange 设置我的 volatile 管道变量)会处理我想为多核 x86 CPU 编写“无锁”代码的所有情况?

我已经知道并使用了关键部分、互斥体、事件等。我主要想知道是否有 x86 内在函数我不知道哪种力或可用于强制缓存一致性。

4

9 回答 9

36

volatile仅强制您的代码重新读取该值,它无法控制从何处读取该值。如果您的代码最近读取了该值,那么它可能会在缓存中,在这种情况下,volatile 将强制它从缓存中重新读取,而不是从内存中读取。

x86 中没有很多缓存一致性指令。有类似的预取指令prefetchnta,但这不会影响内存排序语义。它过去是通过在不污染 L2 的情况下为 L1 缓存带来价值来实现的,但是对于具有大型共享包容性L3 缓存的现代英特尔设计来说,事情变得更加复杂。

x86 CPU 使用MESI 协议的变体(英特尔的 MESIF,AMD 的 MOESI)来保持它们的缓存彼此一致(包括不同内核的私有 L1 缓存)。想要写入高速缓存行的核心必须强制其他核心使其副本无效,然后才能将自己的副本从共享状态更改为修改状态。


您不需要任何栅栏指令(如 MFENCE)来在一个线程中生成数据并在 x86 上的另一个线程中使用它,因为 x86 加载/存储具有内置的获取/释放语义。您确实需要 MFENCE (完全障碍)来获得顺序一致性。(此答案的先前版本建议clflush需要,这是不正确的)。

您确实需要防止编译时重新排序,因为 C++ 的内存模型是弱排序的。 volatile是一种旧的、不好的方法;C++11 std::atomic 是编写无锁代码的更好方法。

于 2009-02-17T21:58:05.247 回答
25

由于 x86 处理器采用 MESI 协议,内核之间的缓存一致性得到了保证。在处理可能在数据仍位于内核缓存上时访问内存的外部硬件时,您只需要担心内存一致性。不过,这里看起来不像是您的情况,因为文本表明您正在用户空间中编程。

于 2009-02-17T22:06:09.550 回答
18

您无需担心缓存一致性。硬件会解决这个问题。您可能需要担心的是由于缓存一致性导致的性能问题。

如果 core#1 写入一个变量,这会使其他核心中缓存行的所有其他副本无效(因为它必须在提交存储之前获得缓存行的独占所有权)。当 core#2 读取相同的变量时,它将在缓存中丢失(除非 core#1 已经将其写回缓存的共享级别)。

由于必须从内存中读取整个缓存行(64 字节)(或写回共享缓存,然后由 core#2 读取),因此会产生一些性能成本。在这种情况下,这是不可避免的。这是期望的行为。


问题是,当您在同一高速缓存行中有多个变量时,即使内核在同一高速缓存行中读取/写入不同的变量,处理器也可能会花费额外的时间来保持高速缓存同步。

通过确保这些变量不在同一个高速缓存行中,可以避免这种成本。这种效果称为错误共享,因为您正在强制处理器同步线程之间实际上未共享的对象的值。

于 2009-02-17T21:54:47.733 回答
7

挥发性不会这样做。在 C++ 中,volatile 只影响编译器优化,例如将变量存储在寄存器而不是内存中,或者完全删除它。

于 2009-02-17T21:51:30.707 回答
6

您没有指定您使用的是哪个编译器,但如果您使用的是 Windows,请在此处查看这篇文章。还可以在此处查看可用的同步功能。您可能要注意,通常volatile不足以做您希望它做的事情,但在 VC 2005 和 2008 下,添加了非标准语义,在读写周围添加了隐含的内存屏障。

如果你想让东西便携,你将面临更加艰难的道路。

于 2009-02-17T21:54:52.290 回答
3

这里有一系列解释现代内存架构的文章包括Intel Core2 缓存和更多现代架构主题。

文章可读性很强,插图也很好。享受 !

于 2009-02-18T13:30:52.510 回答
3

您的问题中有几个子问题,因此我会尽我所能回答。

  1. 目前没有在 C++ 中实现无锁交互的可移植方式。C++0x 提案通过引入原子库解决了这个问题。
  2. Volatile 不能保证在多核上提供原子性,并且它的实现是特定于供应商的。
  3. 在 x86 上,您不需要做任何特别的事情,除了将共享变量声明为 volatile 以防止某些可能破坏多线程代码的编译器优化。Volatile 告诉编译器不要缓存值。
  4. 有一些算法(例如 Dekker)即使在带有 volatile 变量的 x86 上也无法工作。
  5. 除非您确定在线程之间传递对数据的访问是程序中的主要性能瓶颈,否则请远离无锁解决方案。使用按值或锁传递数据。
于 2009-04-13T20:52:40.173 回答
2

以下是一篇关于使用volatile带线程程序的好文章。

易失性对于多线程编程几乎没用

于 2009-09-22T02:18:17.910 回答
1

Herb Sutter 似乎只是建议任何两个变量都应该驻留在不同的缓存行上。他在他的并发队列中执行此操作,并在他的锁和节点指针之间进行填充。

编辑:如果您使用的是 Intel 编译器或 GCC,则可以使用atomic builtins,这似乎会尽可能地抢占缓存。

于 2009-02-17T21:59:31.920 回答