什么缓存一致问题
在谈缓存一致性协议之前我们先了解一下缓存一致性问题是什么,它是怎么出现的。
现在处理器处理能力上要远胜于主内存(DRAM),主内存执行一次内存读写操作,所需的时间可能足够处理器执行上百条的指令,为了弥补处理器与主内存处理能力之间的鸿沟,引入了高速缓(Cache),来保存一些CPU从内存读取的数据,下次用到该数据直接从缓存中获取即可,以加快读取速度,随着多核时代的到来,每块CPU都有多个内核,每个内核都有自己的缓存,这样就会出现同一个数据的副本就会存在于多个缓存中,在读写的时候就会出现数据 不一致的情况。
CPU Cache 和 Cache Line
CPU Cache
缓存名称 | 是还共享 | 描述 |
一级缓存(L1 Cache) | CPU CORE独享 | 制造成本很高因此它的容量有限,但是读取速度很快 |
二级缓存(L2 Cache) | CPU CORE独享 | 是一级缓存的缓冲器,存储那些CPU处理时需要用到、一级缓存又无法存储的数据,读取速度低于一级缓存 |
三级缓存(L3 Cache) | 多个CPU CORE共享的 | 可以看作是二级缓存的缓冲器,读写速度低于二级缓存,CPU主要通过三级缓存与总线通信 |
Cache Line
数据在缓存中不是以独立的项来存储的,它不是一个单独的变量,也不是一个单独的指针,它在数据缓存中以缓存行存在的,也称缓存行为缓存条目。目前主流的CPU Cache的Cache Line大小通常是64字节,并且它有效地引用主内存中的一块地址。一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量
如果需要详细了解缓存行可以参考另一篇文章 聊聊CacheLine
计算机世界的局部性
局部性原理:在CPU访问存储设备时,无论是存取数据或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理。
- 时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。比如程序中的循环、递归对数据的循环访问,主要体现在指令读取的局部性
- 空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。比如程序中的数据组的读取或者对象的连续创建,对内存都是顺序的读写,主要体现在对程序数据引用的局部性
保证缓存一致性的一点儿思考
根据缓存一致性问题的描述,如果可以做到在读取的时候读到最新的数据 ,CPU对同一个共享数据的写入操作只在一个核上运行,并且将更改后的内容及时写回主内存即可。
由于现在超线程技术,一个核可能出现多个线程,平时听到的4核8线程,6核12线程,它是通过在物理CPU核上采用特殊的硬件指令来模拟两个内核运行,旨在利用充分利用CPU闲置资源,所以上面说对同一共享数据的写入只有在一个核上运行并不是很准确,应该是在保证数据只在一个缓存中被修改,并同步回主内存
MESI是什么
MESI
是众多缓存一致性协议中的一种,也在Intel系列中广泛使用的缓存一致性协议缓存行(Cache line)
的状态有Modified
、Exclusive、 Share
、Invalid
,而MESI 命名正是以这4中状态的首字母来命名的。该协议要求在每个缓存行上维护两个状态位,使得每个数据单位可能处于M、E、S和I这四种状态之一,各种状态含义如下:
状态 | 含义 | 描述 |
M | 修改 | 表示缓存行数据被修改了,并且没有更新至主内存。处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没有。简单的可理解为缓存行数据独占被修改且未同步 |
E | 独享(互斥) | 表示缓存行数据是独占的。处于这一状态的数据,只有在本CPU中有缓存,其它CPU中没有缓存该数据,且其数据没有修改与主内存中一致。简单的可理解为缓存行数据独占且未被修改 |
S | 共享 | 表示缓存行数据是共享的。处于这一状态的数据在多个CPU中都有缓存,且与内存一致 |
I | 无效 | 表示缓存行数据是无效的。本CPU中的这份缓存已经无效。 |
上面对于S
状态的描述,我们可能会想到另一种状态,数据被共享但是与内存中不一致的情况,这就是我们MESI协议需要解决的问题
MESI是如果保证缓存一致性
MESI协议对不同的状态增了不同的监听任务,监听任务的规则如下
- 一个处于
M
状态的缓存行,必须时刻监听所有试图读取
该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回主内存
- 一个处于
S
状态的缓存行,必须时刻监听使该缓存行无效
或者独享
该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I
。 - 一个处于
E
状态的缓存行,必须时刻监听其他试图读取
该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S。
MESI消息
消息名 | 消息类型 | 描述 |
Read | 请求 | 通知其它处理器,主内存当前处理器准备读取某个数据。该消息包含待读取数据的内存地址 |
Read Response | 响应 | 该消息包含被请求的读取消息的数据。可能是主内存提供的,也可能是嗅探到Read消息的其它处Cache提供的,主要看嗅探到Read消息的Cache中缓存行的状态 |
Invalidate | 请求 | 通知其它处理器将其高速缓存中指定内存地址对应的缓存行状态置为I(无效) ,也就是通知其它处理器将指定内存地址的副本数据删除 |
Invalidate Acknowledge | 响应 | 接收到Invalidate消息的处理器必须回复此响应,以表示删除了其高速缓存上的相应副本数据(这里的删除是逻辑删除,其实只是更新了缓存条件的Flag值) |
Read Invalidate | 请求 | 从名字可以推断出该消息是一个复合消息,是由Read消息和Invalidate消息组合而成。它的作用是通知其它处理,发送该消息的处理器准备更新一个数据,请求其它处理器删除其高速缓存中相应的副本数据。接收到该消息的处理器必须回复两个响应消息,Read Response、Invalidate Acknowledge消息,发送该消息的处理器期望收到一个Read Response以及多个Invalidate Acknowledge。 |
Writeback | 请求 | 该消息包含需要写入主内存的数据及其对应的内存地址 |
MESI协议的处理流程
在这里我们只讨论多核情况下的数据读取X,我们以两核为例,CPUA 拥有 L1A高速缓存,CPUB拥有L1B高速缓存
MESI协议在数据的读定时,是通过往总线中发送消息请求
和响应
来保证数据的一致性的,下面我们看一下数据的读取流程
数据读取流程
场景
CPUA需要读取数据X
处理流程
CPUA需要读取数据X,会根据数据的地址在自己的缓存L1A中找到对应的缓存行,然后判断缓存行的状态
- 如果缓存行的状态是
M、E、S
,说明该缓存行的数据对于当前读请求是可用的
,直接从缓存行中获取地址A对应的数据 - 如果缓存行的状态是
I
,则说明该缓存行的数据是无效的
,则CPUA会向总线发送Read消息
,说’我现在需要地址A的数据,谁可以提供?‘,其它处理器(CPUB)会监听总线上的消息,收到消息后,会从消息中解析出需要读取的地址,然后在自己缓存(L1B)中查找缓存行,这时候根据找到缓存行的状态会有以下几种情况
- 状态为
S/E
, CPUB会构造Read Response
消息,将相应缓存行中的数据放到消息中,发送到总线同时更新自己缓存行的状态为S
,CPUA收到响应消息后,会将消息中的数据存入相应的缓存行中,同时更新缓存行的状态为S
- 状态为
M
,会先将自己缓存行中的数据写入主内存,并响应Read Response
消息同时将L1B中的相应的缓存行状态更新为S
- 状态为
I
或者在自己的缓存中不存在地址A的数据,那么主内存会构造Read Response
消息,从主内存读取包含指定地址的块号数据放入消息(缓存行大小和内存块大小一致所以可以存放的下),并将消息发送到总线
CPUA获接收到总线消息之后,解析出数据保存在自己的缓存中
写流程
场景
CPUA需要对地址A的X数据进行写操作
处理流程
任何一个处理器执行内存操作时,必须拥有相应数据的所有权。
CPUA会先根据内存地址在自己的缓存中L1A中找相应的缓存行,判断缓存行的不同状态,可能会了现下列几种情况
- 为
E/M
时,说明当前CPUA已经拥有了相应数据的所有权,此时CPUA会直接将数据写入缓存行中,并更新缓存行状态为M,此时不需要向总线发送任何消息。 -
S
时,说明数据被共享,其它CPU中有可能存有该数据的副本,则CPUA向总线发送Invalidate 消息
以获取数据的所有权,其它处理器(CPUB)收到Invalidate消息
后,会将其高速缓存中相应的缓存行状态更新为I
,表示已经逻辑删除相应的副本数据,并回复Invalidate Acknowledge
消息,CPUA收到所有处理器的响应消息后,会将数据更新到相应的缓存行之中,同时修改缓存行状态为E
,此时拥有数据的所有权,会对缓存行数据进行更新,最终该缓存行状态为M
- I
时,说明当前处理器中不包含该数据的有效副本,则CPUA向总线发送
Read Invalidate消息` ,表明”我要读数据X,希望主存告诉我X的值,同时请示其它处理器将自己缓存中包含该数据的缓存行并且状态不是I的缓存行置为无效“
- 其它处理器(CPUB)收到
Invalidate 消息
后,如果缓存行不为I的话,会将其高速缓存中相应的缓存行状态更新为I
,表示已经逻辑删除相应的副本数据,并回复``Invalidate Acknowledge消息` - 主内存收到
Read消息
后,会响应Read Response消息
将需要读取的数据告诉CPUA - CPUA收到所有处理器的
Invalidate Acknowledge消息
和主内存的Read Response消息
后,会将数据更新到相应的缓存行之中,同时修改缓存行状态为E
,此时拥有数据的所有权,会对缓存行数据进行更新,最终该缓存行状态为M
MESI会有哪些问题
从上面处理过程中,其实不难发现,MESI主要是靠在总线上传递消息,并对消息增加不同的监听,来保证一个线程对共享变量的更新,对其它处理器上运行的线程是可见。但是消息传递是要时间的,一个请求,多个响应,每次都会涉及到CPU的切换,对于CPU这么频繁的读取,消息传递产生的时间是一种致命的影响,会导致引起缓存一致性流量风暴,导致各种各样的性能问题和稳定性问题。
MESI总结
CPU除了在做内存数据传输的时候和总线交互 ,而且还会通过不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。当一个缓存代表它所属的处理器去读写内存时,其它处理器都会得到通知,以此来使自己的缓存保持同步
。只要某个处理器一写内存,其它处理器马上知道这块内存在它们的缓存段中已失效。
思考
- 通过上面的描述看来,在多个线程共享变量的情况下,MESI协议已经能够保障一个线程对共享变量的更新对其它处理器上运行的线程来说是可见的;既然如此,JAVA中的可见问题为何会存在?
- MESI频繁的消息请求与响应带来的性能问题如何解决?