问题

多核 CPU 中,每个核心都有自己的 L1/L2 缓存。

如果核心 A 修改了某个变量 X(先写进自己的缓存),核心 B 随后读取 X,它可能读到的是自己缓存中的旧值。

这就是缓存一致性问题。

MESI 协议

最经典的解决方案。每个缓存行(Cache Line)有四种状态:

  • M(Modified,已修改):数据被修改过,与主存不一致,只存在于本缓存中;
  • E(Exclusive,独占):数据与主存一致,且只存在于本缓存中;
  • S(Shared,共享):数据与主存一致,可能同时存在于多个缓存中;
  • I(Invalid,无效):缓存行中的数据已失效。

状态转换

读操作:

  • 缓存行处于 M/E/S:直接命中;
  • 处于 I:发起总线请求,其他核心如果有该行的 M 状态,需要先写回主存(或直接传给请求方),然后请求方获得 S 状态。

写操作:

  • 处于 M:直接写(已经是最新的);
  • 处于 E:直接改为 M(因为独占,无需通知他人);
  • 处于 S:需要向其他核心发送失效(Invalidate)消息,让它们的副本失效,然后转为 M;
  • 处于 I:先读取(获得独占权),再写入。

总线嗅探(Snooping)

各核心的缓存控制器"监听"总线上其他核心的内存操作广播,据此更新自己缓存行的状态。

这是 MESI 的实现基础。

局限:核心数增多时,广播流量成为瓶颈。因此在大规模多核系统中,改用基于目录(Directory-based)的一致性协议:用一个中心目录记录每个内存块被哪些核心缓存,避免广播。

伪共享(False Sharing)

一个经典的性能陷阱。

两个线程分别修改两个不同的变量,但这两个变量恰好落在同一个缓存行内。

由于一致性协议以缓存行为单位维护,核心 A 修改变量 1 会导致核心 B 的整个缓存行失效;核心 B 修改变量 2 又会让 A 的缓存行失效。

结果:两个本不相关的变量,导致缓存行在两个核心之间反复"乒乓",性能急剧下降。

解决:内存对齐填充(Padding),把变量放到不同的缓存行。在高性能并发编程中,这是一个必须掌握的技巧。

内存模型

缓存一致性协议保证了"所有核心看到的内存最终一致",但不保证程序看到的修改顺序符合直觉。

为了性能,处理器和编译器可能对内存操作进行重排序。因此有了内存模型(Memory Model)的概念——它定义了多线程程序中,哪些读写操作的顺序是被保证的。

  • x86:相对较强的内存模型(TSO),重排受限;
  • ARM:更弱的内存模型,允许更多重排,因此需要显式的内存屏障(Memory Barrier)来保证顺序。

这也是为什么并发编程在不同平台上行为可能不同,以及为什么需要使用语言层面的同步原语(互斥锁、原子操作)。

说明:本文为多处理器体系结构的公开知识整理。