1、缓存一致性问题?

为了压榨cpu性能,解决cpu算数逻辑单元(ALU) 直接访问 内存 速度低的问题,cpu 内部有了 高速缓存,把内存中的数据读取到内部 高速缓存 中来,避免直接访问内存,以加快 ALU 频繁读取的速度。我们假设在一台 pc 上只有一个单核的 cpu 和 一份内部高速缓存,那么所有的进程和线程看到的数据都是一致的,不会存在问题。但是现在的服务器通常都是多 cpu,每个cpu里有多个核心,而且每个核心都维护了自己的高速缓存,那么这个时候多线程并发就会存在 缓存不一致性问题,会导致严重问题。

i++ 为例,i 的初始值为 0,两个内核都缓存了 i=0,当第一块内核做 i++ 的时候,其缓存中的值变成了 1, 即使马上回写到主内存,在第一块内核回写之后,第二块内核缓存中的i值依然为 0,其执行 i++,回写到内存就会覆盖第一块内核的操作,使得最终的结果为 1,而不是预期中的 2。

2、解决缓存一致性问题

怎么解决这个问题,在早期的 CPU 当中,是通过 cpu 在总线上发LOCK#信号的形式来解决缓存不一致的问题。因为 CPU 和其他部件进行通信都是通过总线来进行的,如果对总线发LOCK#信号锁定总线的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个 CPU 能使用这个变量的 内存,锁定期间,其他处理器不能操作其他内存地址的数据。

但我们只需要对此共享变量的操作是原子就可以了,而总线锁定把 cpu 和 内存的通信给锁住了,使得在锁定期间,其他处理器不能操作其它内存地址的数据,从而 cpu 效率低下,所以后来的 cpu 都提供了缓存一致性机制(Intel的P6之后就提供了这种优化-缓存一致性机制。)。

缓存一致性协议最出名的就是 Intel 的 MESI 协议,MESI协议 保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

总结:为了解决缓存不一致的问题,在 CPU 层面,主要提供了两种解决办法(这2种方式都是硬件层面上提供的方式。):

2.1、总线锁

cpu向总线发出LOCK#信号,首先锁定总线,此cpu独享内存。

2.2、缓存锁

cpu向总线发出LOCK#信号,首先根据缓存一致性机制,保存操作原子性。共享变量的所有副本一致。

注意:lock指令底层是总线锁还是缓存锁

相比总线锁,缓存锁即降低了锁的力度。核心机制 是基于 缓存一致性协议 来实现的。为了达到数据访问的一致,需要各个处理器在访问缓存时遵循一些协议,在读写时根据协议来操作,常见的协议有 MSI、MESI、MOSI 等。最常见的就是 MESI 协议。
在 MESI 协议中,每个缓存的缓存控制器不仅知道自己的 读写操作,而且也监听(snoop)其它 Cache 的读写操作。

2.2.1 MESI协议

MESI协议是以缓存行(缓存的基本数据单位,在Intel的CPU上一般是64字节)的几个状态来命名的,全名是Modified、Exclusive、 Share or Invalid。该协议要求在每个缓存行上维护两个状态位,使得每个数据单位可能处于M、E、S和I这四种状态之一,各种状态含义如下:

  • M:被修改的。处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没有。同时其状态相对于内存中的值来说,是已经被修改的,且没有更新到内存中。
  • E:独占的。处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改,即与内存中一致。
  • S:共享的。处于这一状态的数据在多个CPU中都有缓存,且与内存一致。
  • I:无效的。本CPU中的这份缓存已经无效。

3、计算机内存模型

参考文档

Java多线程机制缓存一致性和CAS

内存模型的相关概念

lock指令底层是总线锁还是缓存锁

并发编程-(4)-JMM基础(总线锁、缓存锁、MESI缓存一致性协议、CPU 层面的内存屏障)

posted on 2022-03-14 14:39  哑吧  阅读(270)  评论(0编辑  收藏  举报