引言
面试官:CPU1 把变量 a=1 从内存读出改成 a=2,CPU2 想把 a 改成 3,CPU2 如何感知 CPU1 的行为?
在并发的情境下,保证数据的一致性,就是使用锁来控制,这里的锁算是一个比较高层次的概念。现代计算机基本上都是多 CPU 的架构,每一个 CPU 都有自己的缓存,所以每一个缓存从主存中取来数据就出现了类似的数据一致性问题。那么在计算机系统层面,应对这种问题的机制是怎样的呢?
共享内存组织
CPU 和内存存在比较大的速度差异,根据局部性原理,引入了缓存,简化结构如下:
- 通过共享的总线,缓存从内存拷贝数据,内存对于所有缓存是共享的
- CPU 从缓存中取出数据进行工作,每一个 CPU 的缓存都是私有的
这种缓存私有,内存共享的结构就会出现数据一致性的问题。例如 CPU1 和 CPU2 从主存中拷贝一个相同的变量 a=0,CPU1 将其改成 a=1,CPU2 将其改成 a=2,此时不同的 CPU 对于 a 的值就会出现分歧。
Snooping Protocol
一种解决办法是采用监听策略:总线是安全的,CPU 通过总线来进行通信,并且会持续监听总线上与自己缓存相关的信息。在写数据的时候,让其他缓存行失效:
- CPU 准备写入变量,先向总线发送 write invalid 信息
- 其他 CPU 收到这个信息,就将自己的对应缓存行置为 invalid
- CPU 写入变量,并且更新到内存(假设采用 Write Through 策略)
- 其他 CPU 再次像访问这个变量的时候,由于缓存行已经失效,只能从内存获取最新的数据
还有一种时,在写数据的时候,同时更新其他缓存行:
- CPU 准备写入变量,先向总线广播要写入的新值
- 其他 CPU 更新对应的缓存行数据
这两种方式都存在一定可改进的空间:
- 总线带宽宝贵,应该减少过多的广播
- Write Through 策略相比起 Write Back 更有可能频繁访问内存,开销较大
MESI Protocol
CPU 通过总线来监听变量的变化
MESI 能够减少总线的带宽使用,并尽可能减少内存访问。在 Snoop 协议中,如果一个变量只是被某个缓存独享,那么是没有必要广播的。为此,MESI 将缓存行原本的 Valid、Invalid 两种状态拓展为:
缓存行的状态 | 含义 |
---|---|
Modified | 变量被一个缓存独占,且被修改,即与内存中的不一致 |
Exclusive | 变量被一个缓存独占,未被修改,即与内存中的一致 |
Shared | 变量被多个缓存共享,未被修改,即与内存中的一致 |
Invalid | 缓存行已经失效 |
通过增加了 Modified 和 Exclusive 这两种独占的状态,巧妙地减少了信息的广播频率。缓存行的状态就在 MESI 这四种状态中进行切换。回到最初的那个问题,一步步进行解析:
CPU1 把变量 a=1 从内存读出改成 a=2,CPU2 想把 a 改成 3,CPU2 如何感知 CPU1 的行为?
CPU2 通过总线来感知 CPU1 的行为:
- CPU1 向总线发起访存请求,获取到变量 a,缓存行标记为 E
- CPU1 更新 a=2,缓存标记为 M,无需向外广播任何消息
- CPU2 向总线发起访存请求,CPU1 监测到这个请求,将 a=2 这个最新值发送到总线,最新值被缓存到 CPU2,并且被写回内存,缓存行也都标记为 S
- CPU2 尝试更新 a=3,由于缓存行当前为 S,所以 CPU2 广播信息使其他缓存行失效,修改完本地 a=3 后,缓存行标记为 M