前言
- 参考/导流:
小林coding-2.4 CPU 缓存一致性 - 学习意义
- 底层基础知识,了解CPU执行过程,让上层编码有效
- 并发控制底层设计思维(对比 MySQL的并发控制)、更好地去理解JUC的锁、volatile以及JMM
- 架构层面的一致性保证问题,由CPU协调单位上升至线程、进程、机器
- 相关说明
该篇博文是个人阅读的重要梳理,仅做简单参考,详细请阅读小林coding的原文!
四、CPU缓存一致性
CPU Cache 和 内存不一致性
程序先加载至内存中,CPU执行程序需要先读取缓存Cache,若无该数据(内存地址)则读取内存。而计算机中使用 数据 是使用它的信息,可以存在不同地方,一份数据可存在于 Cache和内存,对于读来说,未改变信息的状态【只需保证写的一致性,则读就会一致】,无论它在Cache,还是内存,它们是一致性的。而对于写来说,则会改变数据的状态,此时则会导致 Cache的数据是新数据,内存由于距离远而是旧数据。此时,则产生了 缓存 和 内存 不一致的问题。对于CPU的缓存和内存不一致性的解决办法有:
- 写直达:当缓存更新数据之后,直接写到内存,随时保证缓存和内存的一致性。问题则是加大了写的开销。如果单核情况下,CPU读数据优先从Cache读,本来该数据缓存已有该数据,无需从内存加载,随时保证缓存和内存一致性的必要性也不大。因此,只需考虑当Cache被替换,不再命中时,需要再从内存拿数据时,才更新cache的数据至内存即可。那么对于多核呢?多核则是后续Cache间来保证一致性的讨论。
- 写回:在写直达的基础,写回则是当CacheBlock被替换,需要再从内存拿数据时才去更新至内存【而cacheblock被替换时也不一定是被更改过的,此时则需要去标记该数据是否为脏,若脏则需要写回内存】。这样就会减少由Cache写至内存的频率。增大效率。
Cache的读写策略
写直达
保持内存与 Cache 一致性最简单的方式是,把数据同时写入内存和 Cache 中,这种方法称为写直达(Write Through)。

写回
直达由于每次写操作都会把数据写回到内存,而导致影响性能,于是为了要减少数据写回内存的频率,就出现了写回(Write Back)的方法。
在写回机制中,当发生写操作时,新的数据仅仅被写入 Cache Block 里,只有当修改过的 Cache Block「被替换」时才需要写到内存中,减少了数据写回内存的频率,这样便可以提高系统的性能。

- 如果当发生写操作时,数据已经在 CPU Cache 里的话,则把数据更新到 CPU Cache 里,同时标记 CPU Cache 里的这个 Cache Block 为脏(Dirty)的,这个脏的标记代表这个时候,我们 CPU Cache 里面的这个 Cache Block 的数据和内存是不一致的,这种情况是不用把数据写到内存里的;
- 如果当发生写操作时,数据所对应的 Cache Block 里存放的是「别的内存地址的数据」的话,就要检查这个 Cache Block 里的数据有没有被标记为脏的:
- 如果是脏的话,我们就要把这个 Cache Block 里的数据写回到内存,然后再把当前要写入的数据,先从内存读入到 Cache Block 里。然后再把当前要写入的数据写入到 Cache Block,最后也把它标记为脏;
- 如果不是脏的话,把当前要写入的数据先从内存读入到 Cache Block 里,接着将数据写入到这个 Cache Block 里,然后再把这个 Cache Block 标记为脏。

总结一下Cache的读写策略
Cache写命中(Write Hit)时的策略
-
写直达 (Write Through)
CPU向Cache写数据时,Memory中的内容也将被同步更新。好处是可以保证Cache和Memory的一致性,坏处是影响运行速度。【对比CAP理论,程序设计总是 性能和可用之间做平衡】
-
写回(Write Back)
CPU向Cache写数据时,Memory中的内容将不立刻做同步更新,只做Dirty标记。当Cache块被换出时,才将改动同步到Memory中。好处是保证了程序运行速度,坏处是存在Cache和Memory的不一致性。
Cache写缺失(Write Miss)时的策略
写失效(write miss)即所要写的地址不在cache中。
- Write allocate
把要写的数据载入到Cache中,写Cache,然后再通过flush方式写入到内存中 - No write allocate
直接把要写的数据写入到内存中。
Cache读策略
- Read through
直接从内存中读取数据 - Read allocate
先把数据读取到Cache中,再从Cache中读数据。
多核心的缓存间不一致性
问题引入
现在 CPU 都是多核的,由于 L1/L2 Cache 是多个核心各自独有的,那么会带来多核心的缓存一致性(Cache Coherence) 的问题,如果不能保证缓存一致性的问题,就可能造成结果错误。【对比 分布式架构,由核心 上升至 机器;内部通信 由 总线,到 网络通信】
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G5UseOB6-1671416925411)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9f148b27-7234-44c3-b2db-4f9439f0ac69/Untitled.png)]
这时如果 A 号核心执行了 i++ 语句的时候,为了考虑性能,使用了我们前面所说的**写回策略【**若采用写直达,时刻保证内存的数据最新,是否也会存在该问题?因为在写至内存之前,另外的核心也可能去读内存?也需要锁去保证 顺序性】,先把值为 1 的执行结果写入到 L1/L2 Cache 中,然后把 L1/L2 Cache 中对应的 Block 标记为脏的,这个时候数据其实没有被同步到内存中的,因为写回策略,只有在 A 号核心中的这个 Cache Block 要被替换的时候,数据才会写入到内存里。
如果这时旁边的 B 号核心尝试从内存读取 i 变量的值,则读到的将会是错误的值,因为刚才 A 号核心更新 i 值还没写入到内存中,内存中的值还依然是 0。这个就是所谓的缓存一致性问题,A 号核心和 B 号核心的缓存,在这个时候是不一致,从而会导致执行结果的错误。
解决以上问题,则需要保证以下两点:
- 写传播,当一个核心更新数据,能通知到其他核心。【手段方式,对比RPC中的心跳检测机制,】
- 串行化,保证事务的串行化,让核心执行事务是顺序性的。【目标,手段通过锁—】
要实现事务串行化,要做到 2 点:
- CPU 核心对于 Cache 中数据的操作,需要同步给其他 CPU 核心;
- 要引入「锁」的概念,如果两个 CPU 核心里有相同数据的 Cache,那么对于这个 Cache 数据的更新,只有拿到了「锁」,才能进行对应的数据更新。
具体实现
- 总线嗅探【类似心跳检测,rpc发消息通知,结合着 分布式事务\RPC通信对比】
- MESI协议【对比共识算法、数据一致性算法以及MySQL的ACID特性】
总线嗅探
写传播的原则就是当某个 CPU 核心更新了 Cache 中的数据,要把该事件广播通知到其他核心【时刻监听广播事件】。最常见实现的方式是总线嗅探(Bus Snooping)。
- 广播
- 监听
问题点:
- CPU 需要每时每刻监听总线上的一切活动,但是不管别的核心的 Cache 是否缓存相同的数据,都需要发出一个广播事件。
- 总线嗅探只是保证了某个 CPU 核心的 Cache 更新数据这个事件能被其他 CPU 核心知道,但是并不能保证事务串行化。
MESI协议
在总线嗅探时讨论,对于发广播事件的CPU核心无论别的核心是否用该数据都发生,对于其他CPU核心需要时刻监听广播通知。针对该两种问题,可以去细粒度标记数据 Cacheline的状态,来减少不必要的开销浪费。
MESI则通过以下四个状态来标记 Cache Line 四个不同的状态,来更好地进行广播传播,保证同步以及 实现 串行化。
- Modified,已修改
- Exclusive,独占
- Shared,共享
- Invalidated,已失效
已修改状态,代表该 Cache Block 上的数据已经被更新过(脏标记),但没有写到内存里。
已失效状态,表示的是这个 Cache Block 里的数据已经失效了,不可以读取该状态的数据。
「独占」和「共享」状态都代表 Cache Block 里的数据是干净的,也就是说,这个时候 Cache Block 里的数据和内存里面的数据是一致性的。
「独占」和「共享」的差别在于,独占状态的时候,数据只存储在一个 CPU 核心的 Cache 里,而其他 CPU 核心的 Cache 没有该数据。这个时候,如果要向独占的 Cache 写数据,就可以直接自由地写入,而不需要通知其他 CPU 核心,因为只有你这有这个数据,就不存在缓存一致性的问题了,于是就可以随便操作该数据。
状态转换
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XwiE7751-1671416946964)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/990ffa50-8582-4158-a0f6-6ce62122fbf7/Untitled.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X3hKyULp-1671416925412)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5c97cb7d-a392-40ca-bdb5-34aa36e14f9f/Untitled.png)]](https://img-blog.csdnimg.cn/e62d88f1ccba4ec88a8a09d87ddc97b7.png)
具体示例
- 当 A 号 CPU 核心从内存读取变量 i 的值,数据被缓存在 A 号 CPU 核心自己的 Cache 里面,此时其他 CPU 核心的 Cache 没有缓存该数据,于是标记 Cache Line 状态为「独占」,此时其 Cache 中的数据与内存是一致的;
- 然后 B 号 CPU 核心也从内存读取了变量 i 的值,此时会发送消息给其他 CPU 核心,由于 A 号 CPU 核心已经缓存了该数据,所以会把数据返回给 B 号 CPU 核心。在这个时候, A 和 B 核心缓存了相同的数据,Cache Line 的状态就会变成「共享」,并且其 Cache 中的数据与内存也是一致的;
- 当 A 号 CPU 核心要修改 Cache 中 i 变量的值,发现数据对应的 Cache Line 的状态是共享状态,则要向所有的其他 CPU 核心广播一个请求,要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态,然后 A 号 CPU 核心才更新 Cache 里面的数据,同时标记 Cache Line 为「已修改」状态,此时 Cache 中的数据就与内存不一致了。
- 如果 A 号 CPU 核心「继续」修改 Cache 中 i 变量的值,由于此时的 Cache Line 是「已修改」状态,因此不需要给其他 CPU 核心发送消息,直接更新数据即可。
- 如果 A 号 CPU 核心的 Cache 里的 i 变量对应的 Cache Line 要被「替换」,发现 Cache Line 状态是「已修改」状态,就会在替换前先把数据同步到内存。








![[附源码]Python计算机毕业设计Django校园疫情防范管理系统](https://img-blog.csdnimg.cn/5d9b52770fd846928d3cdf89478dff5d.png)









