问题
多核 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)来保证顺序。
这也是为什么并发编程在不同平台上行为可能不同,以及为什么需要使用语言层面的同步原语(互斥锁、原子操作)。
说明:本文为多处理器体系结构的公开知识整理。