theme: channing-cyan
这是我参与更文挑战的第26天,活动详情查看: 更文挑战。
紧接着上一篇你好,请谈谈volatile关键字?(二)
4.4 缓存一致性协议MESI
现在来正式谈谈MESI,上面说MESI协议具有四个状态,这四种状态指的是4个单词的首字母,具体包括Modified、Exclusive、Shared、Invalid,用2个bit表示,几种状态解释如下。
Modified表示Cache Line有效,数据只存在当前Cache中,并且数据是已经被修改了,与主存中的是不一致的。Exclusive表示Cache Line有效,数据只存在当前的Cache中,数据和主存保持一致的。Shared表示Cache Line有效,数据并不只是存在当前的Cache中,被多个Cache共享,各个Cache与主存数据都一致。Invalid表示当前缓存行已经失效。
在MESI协议中,每个高速缓存的控制器不单单知道自己的读写操作,而且还监听其它高速缓存的读写操作,这个就是嗅探技术,控制器针对当前Cache Line处于的状态进行不同的监听任务。
4.4.1 状态变化流程
到底是如何监听任务的,我们通过一个简单的例子来分析一下。假设我们现在有一个双核的CPU,主存里面存储着一个i的变量,值为1,现在CPU要做一些运算操作,需要将i读取到缓存中。

步骤1:图上CPU1从主存中读取数据到缓存,当前缓存的存储的变量i=1,缓存行的状态时E,也就是独占,时刻监听着有没有其它缓存也要从主存中加载该变量。

步骤2:图上CPU2也试图从主存中读取变量i,加载到缓存中,CPU1监听到这个事件,于是CPU1立刻做出变化,更改状态为S,CPU2也同时读取到数据,状态也为S。此时两个CPU Cache Line 存储的变量i=1,都在监听有没有事件要使缓存自己置为I无效态,或者其它缓存要独享变量的请求。

步骤3:图上CPU1计算完成后,需要修改变量i=2,缓存管理器先设置Cache Line 的状态为M修改态,然后发起事件通知其它CPU,CPU2收到事件通知,设置Cache Line的状态为I无效态。CPU1 监听着其它缓存要读取主内存的事件。CPU2的缓存行因为状态时无效的,所以缓存行失效。

步骤4:图上,CPU2运算要用到变量i,因为存储i的缓存行失效,去主动同步主内存。CPU1收到有其它CPU要读取主存的请求,赶在读取之前,先把修改后的变量同步到主存,同步完以后,主存上的变量i=2,然后CPU1缓存管理器设置缓存行的状态为E。然后按照步骤4,两个CPU的Cache Line最后状态都变为S。

4.4.2 状态变化原则
总的来说,对于CPU读写操作缓存行,MESI协议遵循以下的原则:
CPU读请求:缓存行当前状态处于M E S状态都可以被读取,处于I状态下,CPU只能从主存中读取数据。CPU写请求:缓存行当前状态处于M E状态才可以被直接写,处于I状态下,缓存行已经失效,无法进行读取操作;处于S状态,能写的前提条件是将其它缓存行设置为无效。
4.4.3 MESI 带来的问题
虽然通过MESI协议的四种状态和嗅探技术,实现了缓存的一致性,但也带来一些问题。
上面我们谈到,如果CPU要将计算后的结果写入Cache Line 中,需要发送一个失效的通知给其它存储了相同数据的CPU,并且必须等到他们的状态变更完成后才能进行相应的写入操作,在整个期间,该CPU在同步地阻塞的等待,十分影响CPU的性能。
为了解决阻塞等待的问题,在CPU中又引入了Store Buffer,通过这个buffer,CPU要修改缓存中的值时,只需要将数据写入这个buffer,就可以去执行其它指令了。然后当收到其它CPU修改指定缓存行的状态为I无效态以后,再将buffer的数据存储到Cache Line,然后必要时,再同步到主存中。
这种方案是异步的,解决了CPU同步等待阻塞的问题。但同时也引入了新的问题。
- 因为是一个异步操作,具体什么时候收到其它
CPU状态变更的通知是不明确的,所以导致Store Buffer的数据什么时候写入Cache Line也是不确定。 - 当未收到其它
CPU状态变更之前CPU有可能会来读取数据,首先会从Store Buffer中读,如果没有,再读Cache Line,如果还没有,再读主存。
新的问题,带来的巨大的影响就是指令重排序。
我们通过一个例子分析具体是什么问题。
``` int value =1; bool finish = false;
void runOnCPU1(){ value = 2; finish = true; }
void runOnCPU2(){ if(finish){ assert value == 2; } } ```
我们假设#runOnCPU1、#runOnCPU2 两个方法分别运行在两个独立的CPU上。 我们很容易想到肯定不会有断言执行。当事实真的如此吗,以下是一种可能的场景。
CPU1 缓存行上缓存了两个关键变量,状态如下:
| | value | finish | | --------------- | ----- | ------ | | CacheLine状态 | S | E |
CPU1在执行#runOnCPU1方法时,会先把value=2写入到Store Buffer中,继续执行finish=true这条指令,与此同时,也通知了其它存储相同变量的CPU 设置缓存行的状态为I无效态,并异步的等待执行结果回执。
因为当前存储finish变量的Cache Line的状态为E独占,所以无需通知其它CPU,立刻就能将finish=true写入Cache Line。这个时候CPU2开始执行#runOnCPU2方法,会从主存中读取finish,按照文章上面介绍的状态变化步骤,会轻松读到finish=true,此时两个CPU存储finish的Cache Line状态都为S,并且主存的finish=true。CPU2继续执行assert value == 2;这条指令,首先要去从主存中获取value的值,因为CPU1修改value的值还放在Store Buffer,所以CPU2取到的值会是1。
也就是说,我们能看到的现象是,在方法#runOnCPU1中,finish赋值早于value的赋值,跟我们预期有差异,这个就是指令重排序带来的可见性问题。
这种可见性问题,可以基于JMM(Java 内存模型)的内存屏障去解决,恰恰好,这个就是volatile保证多线程环境下可见性的杀手锏。
篇幅较长,继续阅读请点击【面时莫慌】你好,请谈谈volatile关键字?(四)
哥佬倌,莫慌到走!觉好留个赞,探讨上评论。欢迎关注面试专栏面时莫慌 | Java并发编程,面试加薪不用愁。也欢迎关注我,一定做一个长更的好男人。
MESI协议详解
本文深入解析了MESI缓存一致性协议的工作原理,包括其四种状态的定义与转换流程,以及在多核环境中如何通过嗅探技术实现缓存一致性。此外,还探讨了MESI协议可能引发的问题及解决方案。
250

被折叠的 条评论
为什么被折叠?



