缓存一致性问题

现代 CPU 基本都是多核的,每个物理核拥有自己的 L1 和 L2 高速缓存,各个物理核之间共享 L3 高速缓存和物理内存。每个物理核都可以单独执行进程或线程,此时便产生了缓存一致性问题:同一内存行,可能被多个核读取并放到了各自的缓存中,之后,某个核修改了缓存行,这导致其他核的缓存行的数据

单核 CPU 的缓存一致性策略

单核 CPU 读操作不会导致缓存一致性问题,而写操作会。
MESI 缓存一致性协议 - 图1
在多级缓存中,L2 可以看作 L1 的内存,缓存之间采用 直写 + 非写分配;而缓存和内存之间则采用 写回 + 写分配。

多核 CPU 缓存一致性策略

详细解释参考链接:MESI 协议详解
外国教学 PDF:
MESI.pdf
多核 CPU 缓存一致性问题采取 MESI 缓存一致性协议解决,MESI 协议是基于总线侦听、回写、写传播失效策略的缓存一致性协议。
它的主要思想是将缓存行分为 4 个状态(Modified, Exclusive, Shared 和 Invalid),在发生 4 种事件(本地读 Local Read,本地写 Local Write,远程读 Remote Read 和远程写 Remote Write)时,调整缓存行的状态。

机制

MESI 缓存一致性协议 - 图2

4 种状态

MESI 缓存一致性协议 - 图3

4 种事件

MESI 缓存一致性协议 - 图4
本地核心进行本地读和本地写都会通过总线广播给其他核心,其他核心通过侦听总线得知消息后修改对应缓存行的状态。

状态转换

通过本地读命中,本地读不命中,本地写命中,本地写不命中这 4 种情况展开讨论。
先勤于思考,再看 PDF 中的详细描述。

优化

MESI 缓存一致性协议 - 图5

存储缓存

问题:本地核心进行本地写要广播给所有其他核心并等待响应才能写入本地缓存,等待非常耗时,CPU 空转。
解决:每个核心有一个存储缓存,将要写的新值放入其中,由它来等待其他核心响应,CPU 继续执行其他指令,收到所有其他核的响应后,就把新值写入高速缓存。
变化:由同步、强一致性变成了异步、最终一致性
缺点:存储缓存大小有限,满了之后依然会阻塞 CPU

引入存储缓存导致的新问题:存储缓存中有新值还未写入高速缓存,导致本地读得到的不是最新数据。
解决:本地读先读存储缓存,若存储缓存有这一行,则直接读出;若没有该行则读高速缓存。

失效队列

问题:核心触发远程写事件,把缓存行同步置为 Invalid 非常耗时。
解决:每个核心有一个失效队列,发生远程写事件时,直接把缓存行加入失效队列并发送确认,等空闲时再去把缓存行置为 Invalid
变化:由同步、强一致性变成了异步、最终一致性。失效队列的引入解决了存储缓存易满的缺点。

引入失效队列导致的新问题:失效队列中还有失效缓存行,核心进行本地读时,可能读到已失效的缓存行。
解决:本地读先查找失效队列是否有目标缓存行,若有,说明已失效;若没有,则读高速缓存。

新问题

存储缓存和失效队列的引入解决了 MESI 协议的低性能问题,但是带来了新问题:全局告诉缓存中的数据由同步、强一致性变成了异步、最终一致性,这说明在某时刻全局高速缓存中的数据可能不一致,这对于并发程序的正确性是致命的。
解决:引入内存屏障:写屏障和读屏障。
写屏障要求在进行新的写事务时,强制等待本地核心处理完存储缓存中的写事务。
读屏障要求在进行新的读事务时,强制等待本地核心处理完失效队列中的远程写事务。
内存屏障以机器指令的形式进行工作
内存屏障机制将对共享变量读写的高速缓存的强一致性控制权交给了程序的编写者或者编译器