并发编程知识梳理(三)

一、操作系统初识

3、MESI协议(volatile的可见性是通过MESI协议实现的)

多核CPU存在多个一级缓存,为了保证缓存内部数据的一致,不让系统数据混乱,就引入的MESI

当我们运行一个程序时,我们jvm会将java代码进行汇编转成字节码来使程序运行,但是我们得程序复制读写,还是通过cpu来执行,那cpu是怎么将一个个值读写赋给我们jvm中操作数栈得呢?这里就涉及到CPU数据读取,数据读取就涉及到多核CPU,就涉及到MESI协议

MESI是指四个状态中的首字母

状态描述监听任务
M 修改 (Modified)该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。
E 独享、互斥 (Exclusive)该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。
S 共享 (Shared)该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。
I 无效 (Invalid)该Cache line无效。

四种状态之间的切换

在这里插入图片描述

多核缓存协同操作举例

假设有三个CPU A、B、C,对应三个缓存分别是cache a、b、 c。在主内存中定义了x的引用值为0。

在这里插入图片描述

单核读取

那么执行流程是:

CPU A发出了一条指令,从主内存中读取x。

从主内存通过bus读取到缓存中(远端读取Remote read),这是该Cache line修改为E状态(独享).

在这里插入图片描述

双核读取

那么执行流程是:

CPU A发出了一条指令,从主内存中读取x。

CPU A从主内存通过bus读取到 cache a中并将该cache line 设置为E状态。

CPU B发出了一条指令,从主内存中读取x。

CPU B试图从主内存中读取x时,CPU A检测到了地址冲突。这时CPU A对相关数据做出响应。此时x 存储于cache a和cache b中,x在chche a和cache b中都被设置为S状态(共享)。

在这里插入图片描述

修改数据

那么执行流程是:

CPU A 计算完成后发指令需要修改x.【这里可能存在多个CPU同时修改数据的问题,解决方案就是通过bus总线获取总线锁,只有获取到总线锁的CPU才能发送通知修改数据】

CPU A 将x设置为M状态(修改)并通知缓存了x的CPU B, CPU B将本地cache b中的x设置为I状态(无效)

CPU A 对x进行赋值。

在这里插入图片描述

同步数据

那么执行流程是:

CPU B 发出了要读取x的指令。

CPU B 通知CPU A,CPU A将修改后的数据同步到主内存时cache a 修改为E(独享)

CPU A同步CPU B的x,将cache a和同步后cache b中的x设置为S状态(共享)。

在这里插入图片描述

为什么volatile不能保证原子性

因为当count=0被两个线程count++时,一个CPU可能从内存读取到count=0,并且在寄存器中执行了count++写回到了内存,这时候总线会通知其他读取count=0的cpu提示变量失效,但是存在一种情况,就是可能其他cpu已经将count在寄存器中执行了count++,这时候只是缓存中的变量失效了,但是寄存器中的值仍然需要写入到内存,这时候会就出现执行两次count++,结果是1的情况

当一个CPU通知另一个CPU读取的同一个变量失效,那么在失效之后,会马上去内存读吗?能马上的读到最新的数据吗?

不会,因为在CPU中存在一个缓冲区【Store Bufferes】,处理器把它想要写入到主存的值写到缓存,然后继续去处理其他事情。当所有失效确认(Invalidate Acknowledge)都接收到时,数据才会最终被提交

为什么要引入缓冲区【Store Bufferes】

因为缓存的一致性消息传递是要时间的,这就使其切换时会产生延迟,当一个缓存被切换状态时其他缓存收到消息完成各自的切换并且发出回应消息这么一长串的时间中CPU都会等待所有缓存响应完成。简单说就是通知发出之后需要等到响应,而在等到响应的时间会很长,并且会阻塞CPU,会降低CPU性能,所以引入缓存区,使得在等待响应时,CPU可以处理其他事情。

引入缓冲区的缺陷?

第一、就是处理器会尝试从存储缓存(Store buffer)中读取值,但它还没有进行提交。这个的解决方案称为Store Forwarding,它使得加载的时候,如果存储缓存中存在,则进行返回。

第二、保存什么时候会完成,这个并没有任何保证

MESI优化和他们引入的问题【简单理解就行】

value = 3void exeToCPUA(){
  value = 10;
  isFinsh = true;
}
void exeToCPUB(){
  if(isFinsh){
    //value一定等于10?!
    assert value == 10;
  }
}

试想一下开始执行时,CPU A保存着finished在E(独享)状态,而value并没有保存在它的缓存中。(例如,Invalid)。在这种情况下,value会比finished更迟地抛弃存储缓存。完全有可能CPU B读取finished的值为true,而value的值不等于10。

即isFinsh的赋值在value赋值之前。

这种在可识别的行为中发生的变化称为重排序(reordings)。注意,这不意味着你的指令的位置被恶意(或者好意)地更改。

它只是意味着其他的CPU会读到跟程序中写入的顺序不一样的结果。

顺便提一下NIO的设计和Store Bufferes的设计是非常相像的。

硬件内存模型

执行失效也不是一个简单的操作,它需要处理器去处理。另外,存储缓存(Store Buffers)并不是无穷大的,所以处理器有时需要等待失效确认的返回。这两个操作都会使得性能大幅降低。为了应付这种情况,引入了失效队列。它们的约定如下:

  • 对于所有的收到的Invalidate请求,Invalidate Acknowlege消息必须立刻发送
  • Invalidate并不真正执行,而是被放在一个特殊的队列中,在方便的时候才会去执行。
  • 处理器不会发送任何消息给所处理的缓存条目,直到它处理Invalidate。

即便是这样处理器已然不知道什么时候优化是允许的,而什么时候并不允许。

干脆处理器将这个任务丢给了写代码的人。这就是内存屏障(Memory Barriers)。

写屏障 Store Memory Barrier(a.k.a. ST, SMB, smp_wmb)是一条告诉处理器在执行这之后的指令之前,应用所有已经在存储缓存(store buffer)中的保存的指令。

读屏障Load Memory Barrier (a.k.a. LD, RMB, smp_rmb)是一条告诉处理器在执行任何的加载前,先应用所有已经在失效队列中的失效操作的指令。

void executedOnCpu0() {
    value = 10;
    //在更新数据之前必须将所有存储缓存(store buffer)中的指令执行完毕。
    storeMemoryBarrier();
    finished = true;
}
void executedOnCpu1() {
    while(!finished);
    //在读取之前将所有失效队列中关于该数据的指令执行完毕。
    loadMemoryBarrier();
    assert value == 10;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值