
MESI 协议Modified, Exclusive, Shared, Invalidated是用于解决多核 CPU 缓存一致性问题的协议。它定义缓存行四种状态保证多个 CPU 核心访问共享数据最终一致。Modified已修改缓存行数据已经被修改还没写回主存。数据只存在当前 CPU 缓存和内存不一致。Exclusive独占只有当前 CPU 缓存存有这份数据缓存和内存内容完全一致其他 CPU 没有该缓存行副本。Shared共享多个 CPU 缓存都保存这份缓存行缓存内容和内存保持一致。Invalidated已失效缓存行数据作废不允许使用。简单讲MESI 只保证最终一致性。它能保证所有缓存副本最后会达成一样的值但不能保证修改发生之后立刻全局同时可见。只依靠 MESI 无法消除短暂的可见性窗口所以软件层面必须补充同步语义原子变量、内存屏障。多线程场景一个共享变量在多个 CPU 缓存行处于 Shared 状态。某一个 CPU 想要修改该缓存行必须广播 Invalidate 消息要求其余 CPU 把该缓存行置为失效。 如果 CPU 写操作原地阻塞等待所有 CPU 处理完失效并返回 ACK 之后才继续执行指令多核性能会严重下降。 为了消除这种阻塞CPU 引入存储缓冲区 Store Buffer写指令不必原地卡住把已经计算完成的新值放入 Store BufferCPU 流水线继续执行后续指令等到收到全部其他核心的 ACK 应答之后本地缓存行状态切换为 Modified再将 Store Buffer 内对应条目提交写入 L1 缓存。通俗过程 多个 CPU 共享同一缓存行。某个 CPU 执行写操作算出修改后的新值存入自身私有 Store Buffer这份新值只有本 CPU 可见还没有写入 L1 缓存同时向其他 CPU 广播 Invalidate 失效通知。其余 CPU 收到失效消息立刻回复 ACK 代表 “消息已收到”但并不会马上把本地缓存行标记为 Invalid而是将失效请求丢入失效队列 (Invalidate Queue)排队延后处理。在队列消息处理完成之前本地 L1 缓存行依旧有效可以继续读到旧数据。写 CPU 收集完全部 ACK将缓存行状态从 Shared 切换为 Modified把 Store Buffer 中新数据提交到本地 L1 缓存这时新数据才正式进入缓存子系统MESI 才能够把更新传播给其他核心。由此产生两个造成 “读到旧数据” 的时间窗口写端窗口Store Buffer 还没有提交到 L1 缓存新值还停留在 CPU 私有缓冲区其他 CPU 完全看不到读端窗口读 CPU 已经回复 ACK但失效消息积压在失效队列未处理依旧复用本地旧缓存行。硬件出于性能设计保留了 Store Buffer、失效队列带来短暂可见性延迟。MESI 管缓存副本最终同步但管不了缓冲区带来的短暂延迟因此需要软件通过内存屏障、原子变量补充同步语义。普通变量只有多线程读完全没问题一旦出现写就有可能别的线程不会立刻看到最新修改。多个线程同时写入普通共享变量会产生数据竞争结果未定义。我们业务上理想期望一个线程修改共享变量修改对其余线程立刻可见对应硬件理想状态CPU 修改缓存行后其余 CPU 缓存行马上失效马上获取最新缓存行副本或者从内存读取最新值。但现实硬件做不到 “立刻全局同步” 跨核消息传递、ACK 应答、Store Buffer 提交、失效队列消费都需要时间。硬件优先追求运行速度故意引入缓冲区掩盖延迟“立刻全局可见” 是昂贵的强同步不是硬件默认行为需要我们主动使用 acquire/release、seq_cst 内存序来约束。此时就引入我们的主题内存序列内存序究竟做了什么来解决可见性与指令重排问题。我们结合 MESI 协议讲解 C 的三种内存模型宽松模型memory_order_relaxed获取‑释放模型memory_order_acquire/memory_order_release/memory_order_acq_rel顺序一致模型memory_order_seq_cst原子变量默认内存序① 宽松模型memory_order_relaxed这是约束最弱的模型。它仅仅保证原子变量本身多线程读写不会发生数据撕裂保证操作是原子的不做任何指令重排限制也不会建立先行关系 (happens‑before)。 编译器和 CPU 可以自由对原子操作前后的普通读写做重排。它只能用于单纯计数这类场景不能用来同步其他共享普通变量。② 获取‑释放模型它保留宽松模型原子性保证当release写与acquire读配对成功acquire 读到 release 写入的新值时会建立先行关系 (happens‑before)。⚠️重点它没有全局统一顺序。不同 CPU 看到多组独立原子操作的先后顺序可以不一样只约束这一对配对的读写之间的数据可见性不会强制整个系统所有核心看到完全相同的指令时序。 对应硬件层面约束 Store Buffer 提交顺序但不会强制排空全部缓冲区、不会强制消费完所有失效队列。③ 顺序一致模型memory_order_seq_cst在获取‑释放模型的基础之上额外提供全局总序保证所有 CPU 看到全部原子操作的执行顺序是完全一致的。 硬件上通常会插入完整内存屏障尽可能排空 Store Buffer、推动失效队列消息处理但随之带来更大的性能开销。这也是std::atomic不指定内存序时的默认选项。⚠️重要区分内存序是 C 语言抽象规则标准并不直接规定 CPU 硬件实现。 MESI 协议只管 L1/L2 缓存行的最终一致性而 Store Buffer、Invalidate Queue 带来的指令重排、短暂可见性缺口是 CPU 硬件性能优化产物。 内存序本质是告诉编译器哪些指令不能乱序告诉 CPU需要插入什么样的屏障指令去约束缓冲区的行为以此在软件层面构建出 happens‑before 先行关系。MESI 保证缓存最终一致但解决不了缓冲区造成的短暂 “读到旧数据” 窗口内存序就是用来约束这些硬件优化行为给程序员提供可控的同步语义。原子变量只有 R‑M‑W读‑修改‑写fetch_add/compare_exchange会锁住缓存行单纯的load、store不会锁缓存行。当缓存行处于 Shared 共享状态CPU 要执行 R‑M‑W 时通过 MESI 协议把缓存行切换到 Modified (M) 状态并且对该缓存行施加硬件锁。 在锁持有期间其他 CPU 想要修改该缓存行会被阻塞等待必须等到本次完整 R‑M‑W 事务执行完毕、释放缓存行锁之后才能继续操作。⚠️注意缓存行锁保护的是整套读‑改‑写的不可分割性不是立刻让修改对其他 CPU 全局可见只是保证别的核不能中途插进来修改避免增量丢失。而在进行读操作时依然可能会读到旧值。而单纯的atomic::load、atomic::store没有缓存行锁store依靠 MESI 协议抢夺 M 状态完成写入load直接读取本地缓存副本。它们只保证原子性数据不会出现撕裂不会读到半截更新的垃圾值只能读到完整旧值或完整新值。但不保证修改立刻对其他线程可见。受 CPU 内部Store‑Buffer写缓冲、Invalidate‑Queue失效队列影响其他线程依然可以读到过期旧值并发多个 store 还会发生后写覆盖先写造成前面写入逻辑丢失。我们发现一个CPU的数据是否对其他线程可见主要取决于MESI中的store buffer 是否被执行刷新到L1中并且失效队列是否被执行其他CPU中的状态是否变成INVALID。只要做好这两点·就能实现可见性的调整。读到这里读者可能会说这原子变量和内存序列读也可能读到旧值那我应该用什么办法来保障一旦其他线程读一定能读到新值呢可以额外用一组原子变量来做同步利用 acquire-release 模型建立先行关系。acquire-release 模型保证一旦 load 读到其他线程 release store 写入的值写线程在 release 之前所有内存写操作对当前线程可见。也就是只要读到这个新值写端 CPU 的 release 屏障保证 release 之前的 store 都已经退出 store buffer、提交到 L1 缓存在 ARM 硬件上acquire 屏障会刷新本地失效队列保证后续内存访问不会读到本核残留的旧缓存此时就建立了同步关系进而产生先行关系。我们了解到acquire-release同步原语其实本质上就是阻止cpu重排指令这样当最新的CPU缓存都已经从store buffer中清空被其他线程读到之前的指令就一定已经在store buffer中被清空了。在写层面上因为后面的操作不会被排到前面去自然也就建立了happens-before关系。