多核CPU场景下,cache如何保持一致、不冲突

9 阅读4分钟

在多核 CPU 场景下,每个核心都有自己的私有缓存(L1、L2),而它们共享 L3 缓存和主内存。要保证不同核心缓存中的数据一致且不冲突,主要依靠 Cache Coherence Protocol(缓存一致性协议)

最经典、应用最广的是 MESI 协议

1. MESI 协议:缓存行的四种状态

MESI 通过给每个缓存行(Cache Line)标记四个状态来管理:

状态名称描述
MModified (修改)该行已被修改,与内存不一致,且只存在于当前核心缓存中。
EExclusive (独占)该行与内存一致,且只存在于当前核心缓存中。
SShared (共享)该行与内存一致,且可能存在于多个核心缓存中。
IInvalid (无效)该行数据已失效,不能使用,必须重新从内存或其它缓存读取。

2. 核心机制:总线嗅探 (Bus Sniffing)

仅仅有状态是不够的,核心之间必须“交流”。

  • 动作:当核心 A 修改了自己缓存中的共享数据时,它会通过总线向外广播。
  • 嗅探:核心 B 会持续监听总线。一旦发现总线上有针对自己缓存中某行数据的写操作,核心 B 会立即将该缓存行标记为 Invalid (I)
  • 结果:下次核心 B 读取该数据时,发现是无效状态,就会被迫从内存或核心 A 那里获取最新的值。

3. 如何避免“冲突”?

多核竞争同一份数据时,硬件通过以下两级保障来防止冲突:

A. 内存屏障 (Memory Barriers)

编译器或 CPU 为了优化性能可能会指令重排。程序员(或底层库)使用内存屏障强制规定:屏障前的读写必须先于屏障后的读写。这确保了逻辑上的先后顺序。

B. 原子操作 (Atomic Operations)

在硬件层面,通过 Lock 前缀指令

  1. 总线锁定:早期 CPU 会锁住总线,让其他核心无法访问内存。效率低,因为整个 CPU 停摆了。
  2. 缓存锁定(现代主流):利用 MESI 协议,如果数据在缓存内,CPU 只锁定该缓存行并标记为独占修改模式。此时其他核心无法同时修改这块内存,保证了操作的原子性。

4. 潜在的问题:伪共享 (False Sharing)

这是高性能编程中常见的“不冲突但低效”的问题。

  • 现象:两个无关的变量(比如 A 和 B)恰好落在同一个缓存行(通常 64 字节)里。
  • 后果:核心 1 修改 A,导致核心 2 的缓存行失效;核心 2 修改 B,又导致核心 1 的缓存行失效。
  • 解决缓存行填充(Padding) 。在变量之间加入无意义的占位符,让它们分布在不同的缓存行。

总结

多核一致性 = MESI 状态机 + 总线嗅探机制

为了让你直观理解 MESI 协议 是如何运作的,我们假设有一个变量 x = 10 存储在主内存中。现在有两个 CPU 核心(Core A 和 Core B)都要操作它。

以下是这四种状态随操作变化的完整链路:

1. 初始状态:独占 (Exclusive)

  • 动作:Core A 从内存读取 x
  • 结果:Core A 发现没有其他核心加载过这个变量。
  • 状态:Core A 的缓存行标记为 E
  • 解释:此时数据是干净的(和内存一致),且 Core A 独占它。

2. 协作状态:共享 (Shared)

  • 动作:Core B 也从内存读取 x
  • 结果:Core A 嗅探(Sniffing)到了这个读取请求,并告诉 Core B:“我也存着呢”。
  • 状态:Core A 和 Core B 的缓存行都转变为 S
  • 解释:大家都知道这份数据在多个核心里都有,且都与内存一致。

3. 修改状态:修改 (Modified)

  • 动作:Core A 想要执行 x = 20

  • 过程

    1. Core A 向总线发送一个“我要写这个地址”的信号。
    2. Core B 嗅探到信号,立即将其缓存行标记为 I (Invalid)
    3. Core A 修改数据。
  • 状态:Core A 变为 M,Core B 变为 I

  • 解释:此时 Core A 里的数据是最新的,但主内存里还是旧的 10。Core A 负有将数据写回内存的责任。

4. 强制失效:无效 (Invalid)

  • 动作:Core B 现在想读取 x

  • 过程

    1. Core B 发现自己的状态是 I(数据不可信)。
    2. Core B 发起读取请求。
    3. Core A 拦截该请求,先将最新的 x = 20 同步回内存(或直接传给 Core B)。
    4. Core A 和 Core B 再次回到 S 状态。
  • 状态:双端回到 S

  • 解释:这就是“缓存一致性”的体现,Core B 绝不会读到旧的 10。