思考内存屏障

背景

前一阵在看JAVA中关于Volatile的实现,往下深究引出内存屏障的概念,查了些资料感觉说的模模糊糊,结合其他文章和自己的一些思考,在脑中设计了几个例子,说明问题。

概念

什么是内存屏障 ?(把欠下的债都结清,然后再开始新的)
在CPU层面,CPU会对指令进行重排序以提高运行效率。这里的CPU包括单CPU,多核CPU。
在多核多CPU中,在某些情况下,这种重排序后的CPU指令乱序执行会导致 “内存的乱序访问”,“内存的乱序访问” 会引发非预期的计算结果。因此,需要引入一种机制,使得在某指令执行前,其他的指令全部执行完毕后才能继续。或者在某指令执行完毕后,其他的指令才能继续执行。PS:上面的话有点绕?往后看后面的例子相信你多少能理解。

内存屏障的分类
不同的抽象层,内存屏障都有自己定义,基本大同小异。
intel CPU硬件层,内存屏障机制由lfence、sfence、mfence指令实现。
编译器中,特定语法在编译后,会加入屏障。
汇编中,lock指令自带内存屏障语义。
JAVA中,JMM标准中定义了四种内存屏障LL、SS、LS、SL。

lfence、sfence屏障指令的含义
Lfence中的L代表Load,Sfence中S代表Store,Mfence中M代表Mixed,包含前2者。
Store大概意思:从Cache存到Mem中。Load大概意思:从Mem读到Cache中。

sfence
多线程执行下面的代码(伪代码):
int a = 0 ---------- 1 线程A
int b = 0; --------- 2 线程B
sfence ------------ 屏障指令
int x = 1; ---------- 4 线程A
解释(1,2,4号指令都是赋值写指令):
在12指令和4指令中插入sfence屏障,4号指令执行前,1,2指令必须按顺序执行,且执行完毕后,4号指令才能执行。这里的Sfence具体指的是屏障前的写指令(1,2)必须将变量值从cache存到MEM中,后面写指令(4)才能执行。sfence对读操作不起作用。

lfence
同理,多线程执行下面的代码(伪代码):
int m = a ---------- 1 线程A
int n = b; --------- 2 线程B
lfence ------------ 屏障指令
int x = a; ---------- 4 线程B
int y = 10; --------5 赋值写指令 线程A
解释(1,2,4都是读写执行,因为读完了a,b的值才能赋值给m,n这里重点考虑读):
当要将a,b分别赋值(写)给m,n时,需要先读,由于lfence指令,需要先将Cache中的a,b设置为失效(MESI中状态为I),然后从mem中重新按顺序读一次a,b的值到Cache中,赋值给m,n,都完事儿后,才能继续读a的值再赋值给x。5号指令是个写指令,因此lfence对5号指令不起作用,5号指令理论上可能会重排序后先于1或2或4执行。

lfence、sfence搭配使用
看过一些其他文章,里面说到了其实大多数情况下,lfence sfence都是搭配使用,如果不搭配用咋样?举个例子说明:

例1:
int x = 0; -------1 线程A
x = 1; --------- 2 线程A
sfence ------------ 屏障指令,只有写屏障
if ( x == 1 ) ---------- 3 线程B
{

}
解释:
1,2号指令是赋值(写)指令,3号指令是读指令; 线程A执行1,2,线程B执行3
1 2指令 和 3指令之间只有个sfence(写屏障)。
那么执行顺序就是1,2按顺序执行,3由于是读操作所以不受sfence影响,3有可能提前执行,因此线程B中执行的 if(x ==1)读到的x有可能是0,也可能是1。

假如只用lfence呢?
例2:
int x = 0; -------1 线程A
x = 1; --------- 2 线程A
lfence ------------ 屏障指令,只有读屏障
if ( x == 1 ) ---------- 3 线程B
{

}
解释:
1,2指令都不是读操作,因此不受lfence约束,3指令是读操作,因此3之前的读必须按顺序都读完才能执行3,在例2中一共就3条指令,3之前没有读操作了,因此实际执行顺序有可能是:312,123,对应x读到值可能是0,1。

解决方法:
1,2指令和3指令间插入mfence,mfence 结合lfence和sfence的语义:mfence之前的读写指令不能重排序,按顺序执行后,后面的读写指令才能继续执行。

备注:
所谓的sfence写屏障,重点是按顺序写且写完(写完的意思是从Cache写到Mem)
所谓的lfence读屏障,重点是按顺序读且读完(读完的意思当前Cache中旧数据失效,从Mem读到Cache)

关于JAVA中Volatile

在JAVA规范中规定:

  1. 当读一个volatile变量时,需要在前面插入一个LL屏障,后面插入一个LS屏障。
  2. 当赋值写一个volatile变量时,需要在前面插入一个SS屏障,后面插入SL屏障。

上面的鬼话啥意思?假设 int volatile a;我粗浅的理解:

  1. a的值从MEM刷到Cache被其他CPU可读到前,其他所有从MEM刷到Cache的动作都要做完,当 a的值刷到了Cache 对其他CPU可见后, 其他CPU才能写(Lock自己的Cache 改了a再刷到MEM中)。
    2.a的值从Cache写到MEM前,其他所有从Cache写MEM的事儿都要做完,当a的值写到MEM后,其他CPU才能读(从MEM中读a到自己的Cache中)。

猜你喜欢

转载自blog.csdn.net/qq_29047189/article/details/106872643