Mesi协议和内存屏障都是和计算机并发相关的概念。
而随着CPU的发展,CPU逐渐发展成了多核,CPU可以同时使用多个核心控制器执行线程任务,当然CPU处理同时处理线程任务的速度也越来越快了,但随之也产生了一个问题,多核CPU每个核心控制器工作的时候都会有自己独立的CPU缓存,每个核心控制器都执行任务的时候都是操作的自己的CPU缓存,CPU1与CPU2它们之间的缓存是相互不可见的。
解决这个问题的根本其实就是需要一种机制来保证一个人修改了内存数据后另外几个缓存了该共享变量的人可以感知到,那么就可以保证各个缓存之间的数据一致性了。
如果想要每个CPU的缓存数据一致,那么最直接的办法就是同时只允许一个人修改内存的数据,当前面一个人操作结束之后,然后 通知其它缓存了该共享变量的缓存,通过这种串行化的方式加上通知机制来保证各个缓存之间的数据一致性,这也就是总线锁的思路。
总线锁模型带来的问题就是效率较低,一个指令周期只允许一个cpu进行内存的读写操作。
与总线锁模型类似,Mesi协议也是一种缓存一致性协议,但更高效。
在cpu读写数据时,需要确保同一数据在多个cache中是一致的。在写之前必须先让其他 CPU cache 中的该数据失效,之后才可以安全的写数据。
cache line的四种状态
M : modified(独占且已更改的数据)
E : exclusive(独占且未更改的数据)
S : shared(共享数据,只读)
I : invalid(无效数据)
|
cacheline数据 |
memory数据 |
直接写数据 |
|
modified |
是 |
最新 |
最新 |
可以 |
exclusive |
是 |
最新 |
最新 |
可以 |
shared |
否 |
最新 |
最新 |
不可以 |
invalid |
否(无数据) |
无数据 |
最新 |
无数据 |
当cpu执行一条write操作时,需要先发送一条invalidate消息,其他所有的 CPU 在收到这个 Invalidate 消息之后,需要将自己 CPU local cache 中的该数据从 cache 中清除,并且发送消息 acknowledge 告知 CPU 0。CPU 0 在收到所有 CPU 发送的 ack 消息后会将数据写入到自己的 local cache 中。
这里就产生了性能问题:当 CPU 0 在等待其他 CPU 的 ack 消息时是处于停滞的(stall)状态,大部分的时间都是在等待消息。为了提高性能就引入的 Store Buffer。
store buffer 的目的是让 CPU 不再操作之前进行漫长的等待时间,而是将数据先写入到 store buffer 中,CPU 无需等待可以继续执行其他指令,等到 CPU 收到了 ack 消息后,再从 store buffer 中将数据写入到 local cache 中。
Store Buffer 的确提高了CPU的资源利用率,不过优化了带来了新的问题。在新数据存储在Store Buffer里时,如果此时有一条read指令,若仍旧从cache中读取数据时,读到的是旧的数据。要解决这个问题就必须要求CPU读取数据时得先看Store Buferes里面有没有,如果有则直接读取Store Buferes里的值,如果没有才能读取自己缓存里面的数据,这也就是所谓的“Store Forward”。
CPU其实不需要完成invalidate操作就可以回送acknowledge消息,这样,就不会阻止发生invalidate请求的那个CPU进入无聊的等待状态。CPU可以buffer这些invalidate message(放入Invalidate Queues),然后直接回应acknowledge,表示自己已经收到请求,随后会慢慢处理。
一旦将一个invalidate(例如针对变量a的cacheline)消息放入CPU的Invalidate Queue,实际上该CPU就等于作出这样的承诺:在处理完该invalidate消息之前,不会发送任何相关(即针对变量a的cacheline)的MESI协议消息。
内存屏障可以简单的认为它就是用来禁用我们的CPU缓存优化的,使用了内存屏障后,写入数据时候会保证所有的指令都执行完毕,这样就能保证修改过的数据能即时的暴露给其他的CPU。在读取数据的时候保证所有的“无效队列”消息都已经被读取完毕,这样就保证了其他CPU修改的数据消息都能被当前CPU知道,然后根据Invalid消息判断自己的缓存是否处于无效状态,这样就读取数据的时候就能正确的读取到最新的数据。
当CPU执行load memory barrier指令的时候,强制其后的store指令,一定是在写屏障之前的所有store指令完成之后,才允许执行。为了达到这个目标有两种方法:方法一就是让CPU stall,直到完成了清空了store buffer(也就是把store buffer中的数据写入cacheline了)。方法二是让CPU可以继续运行,不过需要在store buffer中做些文章,也就是要记录store buffer中数据的顺序,在将store buffer的数据更新到cacheline的操作中,严格按照顺序执行,即便是后来的store buffer数据对应的cacheline已经ready,也不能执行操作,要等前面的store buffer值写到cacheline之后才操作。
当CPU执行load memory barrier指令的时候,对当前Invalidate Queue中的所有的entry进行标注,这些被标注的项次被称为marked entries,而随后CPU执行的任何的load操作都需要等到Invalidate Queue中所有marked entries完成对cacheline的操作之后才能进行。
Memory barrier其实是解决复杂(优化后)cpu架构带来的新问题。它连同Mesi协议一起保障并发程序中数据缓存的一致性。
1、mutex的实现方式
2、memory order
1、带你了解缓存一致性协议 MESI
2、内存屏障(Memory Barrier)究竟是个什么鬼? - 知乎
3、并发理论基础:并发问题产生的三大根源 - 知乎
4、并发基础理论:缓存可见性问题、MESI协议、内存屏障 - 知乎