C2- CPU缓存一致性原理

2023-03-16  本文已影响0人  小龙的城堡

一些常识

CPU

什么是MESI


public class shutDownThread implements Runnable {
    volatile int shutDownRequested;
    volatile int counter;
    public void shutDown(){
        shutDownRequested = true;
    }
    @Override
    public void run() {
        while (!shutDownRequested) {
            System.out.println(counter++);
        }
    }
}


public class Demo01 {
    public static void main(String[] args) throws InterruptedException {
        Thread[] th = new Thread[10];
        shutDownThread t = new shutDownThread();
        for(int i=0;i<=9;i++){
            th[i] = new Thread(t);
            th[i].start();
        }
        Thread.sleep(1000);
        t.shutDown();
    }
}

借用一下这段经典的程序说明下为什么缓存行存在同步问题:

  1. Thread1将共享变量counter从内存加载进入core1核心的C1缓存行;
  2. 然后Thread2也将counter从内存加载进入core2核心的C1缓存行;
  3. Thread1对counter加1,然后将C1写回;此时内存中counter为1;
  4. Thread2也对counter加1,接着写回C1,此时内存counter还是1;
  5. 而我们想让Thread1与Thread2合作,将counter加到2改怎么做?对,进行同步才行,所以CPU也会有这个同步的问题,事实上只要是对共享变量有多个计算单元进行操作,都会产生同步问题,JVM的锁工作在高层,粒度更粗;而CPU的多核同步机制工作在底层,粒度更小而已机制都是一样的。
  6. 多说一点,上面这段代码在有MESI的情况下也不能实现线程同步,原因是counter++这条语句看上去只有一句,但是对于CPU至少有三条:
mov [counter] eax; //加载counter从内存到寄存器(这条mov还会继续分解成操作CL的微指令)
add 1 eax  //+1
mov eax [counter] //写回操作

可见counter++在执行的过程中,可能会被打断(比如时钟中断)造成不可能是原子的。这时,你可能会问MESI的作用是啥?

  1. 如果java可以写汇编,只要将counter++改成lock inc [counter]就能立马变成原子操作了,这就是MESI协议在做影响。有兴趣的朋友可以试试看(intel x86 cpu哦)
  2. 这里给个C++的实现,没有用到C++的任何锁,但是用汇编调起CPU原子锁来做同步:
#include <iostream>
#include <string>
#include <thread>

const int TC = 4;
int count = 0;
bool flag = true;
void inc_count()
{
    asm("movl   $1, %eax");
    asm("LOCK addl %eax,count(%rip)"); //count是个寄存器相对寻址。lock prefix是CPU原子指令。
}
void testRun()
{
    for(int i=0; i<10000;i++)
    {
        inc_count();
    }
}

int main()
{
    std::thread threads[TC]; // 默认构造线程
    for (int i = 0; i < TC; ++i)
    {
        threads[i] = std::thread(testRun); // move-assign threads
    }

    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "main thread id:" << std::this_thread::get_id() << std::endl;
    flag = false;
    for (auto &thread : threads)
    {
        thread.join();
    }

    std::cout << "All threads joined!"
              << " count:" << count<<std::endl;
}

下面解释MESI是如何工作的

首先解释下MESI的含义

Modified(M):更新状态
  1. 整个多核系统中最多只有一个CL的状态是M;其他的CL副本都是I状态,此时CL的值跟内存的值不相等;
  2. 在变到M之前应该先获取CL的所有权,也就是先必须到E;这个过程是通过总线嗅探机制完成,类似队列的监听,所有core都会通过L3环形总线嗅探来自其他core的同步消息。这个过程简单来说是个共识的过程,如果其他core都同意本core对CL的修改,就会将自己的状态变成I,而发起core的状态变成M
  3. 当发起core更新CL完毕,此时core会发起write back回写到内存,同时发消息给其他core通知它们拉取最新的CL值。最后都更新完毕后会回到S状态;
  4. 这里要注意一个细节,就是write back到内存是个漫长的过程,所以采用异步机制回写,提高性能,具体步骤是:将CL回写请求发给store buffer然后就算完成了;然后环形总线处理store buffer,回写到内存。
Exclusive(E) 独占状态
  1. M状态一样,整个SMP core中对于某个CL只能有一个副本的状态处于E这个状态;
  2. 处于E状态下的CL存储的值跟内存中的值是一样的;
  3. 处于E状态的CL是唯一可以转换成M状态的状态;
  4. 如果其他core也发起读取这个内存块的值请求,则这个CL副本会通过环形总线同步给其他core,此时所有这个CL的副本状态都变成S
  5. 如果本地core对CL的数据进行更改,则本地的core的状态变成M。同时会发送消息给其他的core,通知它们将这个CL的状态设置成I
Shared(S) 共享状态
  1. S状态下,CL的数据跟内存中的一致;
  2. S状态说明CL的所有副本都是S状态,所有CL的数据一致;
Invalid(I)失效状态
  1. I状态CL说明本地CL已经已经失效,不能使用,等待更新完毕的通知。

状态机

发布订阅模式

MESI协议的实现很像一个队列服务(其实队列发布订阅异步模型的基础,不论在互联网分布式系统中还是操作系统中都占有重要的地位,可以说只要有异步模型,就必定会有队列。参考)。

  1. core发布的消息(可以简单理解为core发起读、写动作)
  1. core订阅的消息(通过L3总线嗅探机制,其实就是监听某个其他核心群发的事件消息)

转换场景(简化版,详细版本可以参考wiki的解释)

读取场景
cache miss读取场景
S E M状态下的读取
S E M状态下的读取
I状态下的读取
I状态下的读取
更新场景
1.总线发送更改请求 2. 修改完成都是S状态
Store buffer与Invalidate queues

MESI协议的修改过程有两个性能瓶颈:

  1. 发生在修改侧:修改发起core必须发送busWr来跟其他core进行协同共识,这时发起core必须等到所有的ack消息集齐后才能开始动手修改,这就出现了阻塞;
  2. 发生在接收侧:当其他core收到busWr消息后,必须要响应ack。这个过程跟队列接收消息很类似,如果此刻core还有其他的消息要处理,这个消息只能排队,如果积压比较严重,就会pending比较长的时间,发生阻塞。
    [图片上传失败...(image-ae81c2-1679042340818)]
缺点

总结

参考

补充

重排序

CPU为了提高执行效率与指令的吞吐量,发明了很多黑科技其中指令重排序就是一个,而指令重排序中有一个就是Store buffer与Invalid queue的副作用导致的。

还有哪些重排序?

上一篇 下一篇

猜你喜欢

热点阅读