在多线程程序中,多个核心可能同时访问和修改同一份数据,而每个核心都有自己的高速缓存。为了保证各个缓存中的数据副本不会互相矛盾,现代CPU采用了MESI这类缓存一致性协议。它定义了缓存行在不同核心间的状态流转规则,直接影响线程间通信的延迟与吞吐。

一、MESI协议的基本状态与转换
MESI是Modified、Exclusive、Shared、Invalid四个单词的首字母缩写,分别代表缓存行的四种状态。Modified表示当前核心独占该缓存行且已修改,与主存不一致;Exclusive表示独占但未修改;Shared表示可能被多个核心共享且干净;Invalid表示该缓存行无效,必须重新加载。
当核心A读取一个不在缓存中的变量,若其他核心无副本,则进入Exclusive;若其他核心有副本,则大家变为Shared。若核心A要写入,需先发Invalidate消息让其他核心的副本变Invalid,自己转为Modified。这个过程经由总线或互联网络广播,会引入延迟并占用带宽。
1.1 状态切换带来的性能成本
每次写操作触发Invalidate广播,其他核心收到后清空对应缓存行,若它们正要使用这些数据,就必须重新从主存或更高级缓存拉取。这种强制同步使写频繁的热点变量成为性能瓶颈。
以下伪代码展示两个线程反复修改相邻变量的情形,在缓存行64字节架构下极易引发伪共享:
#include <thread>
#include <atomic>
struct BadLayout {
std::atomic<int> a; // 与b可能在同一缓存行
std::atomic<int> b;
};
BadLayout bl;
void thread_a() {
for (int i = 0; i < 1000000; ++i) {
bl.a.fetch_add(1, std::memory_order_relaxed);
}
}
void thread_b() {
for (int i = 0; i < 1000000; ++i) {
bl.b.fetch_add(1, std::memory_order_relaxed);
}
}
二、伪共享如何放大MESI开销
伪共享是指不同线程修改位于同一个缓存行的不同变量。由于MESI以缓存行为最小一致性单位,一个核心写变量会让整个缓存行在其他核心失效,即使对方改的是无关变量。结果两个线程看似无竞争,却因缓存反复失效而大幅降速。
解决方式通常是缓存行填充或对齐。C++中可用alignas(64)让变量独占缓存行,也可用填充字段隔开。下面代码将a与b分到不同缓存行:
#include <thread>
#include <atomic>
struct GoodLayout {
alignas(64) std::atomic<int> a;
alignas(64) std::atomic<int> b;
};
GoodLayout gl;
void thread_a() {
for (int i = 0; i < 1000000; ++i) {
gl.a.fetch_add(1, std::memory_order_relaxed);
}
}
void thread_b() {
for (int i = 0; i < 1000000; ++i) {
gl.b.fetch_add(1, std::memory_order_relaxed);
}
}
2.1 填充的利弊
填充减少失效流量,提升并发写性能,但浪费缓存容量。在缓存紧张或变量极多时,盲目对齐反而降低命中率。因此应针对真正高频并发修改的变量做优化,而非全局使用。
另外,一些编译器与运行时提供了线程本地存储(TLS),将变量设为线程私有可彻底避免跨核同步。若数据无需线程间共享,优先使用线程本地而非原子变量。
三、从架构角度降低MESI影响
除了代码层规避,系统设计中也可减弱一致性协议压力。比如采用无锁环形队列时,让生产者与消费者操作不同缓存行上的指针;或将写密集任务绑定到同一核心,减少跨核失效。
下表对比几种常见手段的适用场景:
| 手段 | 原理 | 适用情况 |
|---|---|---|
| 缓存行对齐 | 隔离变量避免伪共享 | 高频并发写不同变量 |
| 线程本地存储 | 取消共享 | 数据不需跨线程 |
| 核心绑定 | 同核处理相关写 | 任务可集中调度 |
3.1 实际测量方法
开发者可用perf等工具观察cache-misses与bus-cycles,若某项线程化后cache失效飙升,多半是MESI或伪共享作祟。结合火焰图定位热点结构,再决定是否重构布局。
总之,MESI保障了多核正确性,却也带来隐性成本。弄清状态机与广播机制,才能在并发性能调优时做出合理取舍。