设为首页 加入收藏

TOP

内存屏障和 volatile 语义(一)
2018-10-06 21:33:06 】 浏览:201
Tags:内存 屏障 volatile 语义

背景

在阅读java中volatile的关键词语义时,发现很多书中都使用了重排序这个词来描述,同时又讲到了线程工作内存和主存等等相关知识。但是只用那些书的抽象定义进行理解时总是感觉什么地方说不通,最后发现,是那些书中使用的抽象屏蔽了一些对读者的知识点,反而导致了理解上的困难。因此有了这篇文章。没有任何虚构的理解抽象,从硬件的角度来理解什么是内存屏障,以及内存屏障如何让volatile工作。最后说明了在多线程中,如何使用volatile来提升性能。

存储结构

在计算机之中,存在着多级的存储结构。这是为了适应不同硬件速度带来的差异。底层是主存(也就是内存),容量最大,速度最慢;中间是cpu的缓存(现代cpu都有多级缓存结构,级别越高速度越慢,但是可以将多级缓存看成是一个整体),容量较小,但是速度很快;最上层是cpu自身的store buffer和Invalidate Queues,速度最快,容量非常少。其中主存和cpu缓存的数据视图对于每一个cpu都是相同的,也就是说在这个级别上,每个cpu都看到了相同的数据,而store buffer和Invalidate Queues是每一个cpu私有的。这就导致了一系列的编程问题,下文会详细展开。

CPU缓存

Cpu为了平衡自身处理速度过快和主存读写速度过慢这个问题,使用了缓存来存储处理中的热点数据。cpu需要处理数据的时候(包含读取和写出),都是直接向缓存发出读写指令。如果数据不在缓存中,则会从主存中读取数据到缓存中,再做对应的处理。需要注意的是,cpu读取数据到缓存中,是固定长度的读取。也就是说cpu缓存是一行一行的载入数据进来。因为也称之为cpu缓存行,即cacheline。而缓存中的数据,也会在合适的时候回写到主存当中(这个时机可以抽象的认为是由cpu自行决定的)。

现在的cpu都是多核cpu,为了在处理数据的时候保持缓存有效性,因此一个cpu需要数据而且该数据不在自身的cache中的时候,会同时向其他的cpu缓存和主存求取。如果其他的cpu缓存中有数据,则使用该数据,这样就保证了不会使用到主存中的错误的尚未更新的旧数据。

而各个cpu的内部缓存依靠MESI缓存一致性协议来进行协调。以此保证各个Cpu看到的内容是一致的。

Store buffer

如果一个Cpu要写出一个数据,但是此时这个数据不在自己的cacheline中,因为cpu要向其他的cpu缓存发出read invalidate消息。等待其他的cpu返回read response和invalidate ack消息后,将数据写入这个cacheline。这里就存在着时间的浪费,因为不管其他的cpu返回的是什么数据,本cpu都是要将它覆盖的。而在等待的这段时间,cpu无事可干,只能空转。为了让cpu不至于空闲,因为设计了store buffer组件。store buffer是每个cpu独享的写入缓存空间,用于存储对cacheline的写入,而且速度比cacheline高一个数量级,但是容量非常少。

但是store buffer会产生在单核上的读写不一致问题。下面是模拟

a = 1;
b = a+1;
assert b==2;

假设a不在本cpu的cacheline中。在其他cpu的cacheline中,值为0.会有如下的步骤

序号 操作内容
1 发现a的地址不在本cpu的cacheline中,向其他的cpu发送read invalidate消息
2 将数据写入store buffer中
3 收到其他cpu响应的read response和invalidate ack消息
4 执行b=a+1,因为a这个时候已经在cacheline中,读取到值为0,加1后为1,写入到b中
5 执行assert b==2 失败,因为b是1
6 store buffer中的值刷新到a的cacheline中,修改a的值为1,但是已经太晚了

为了避免这个问题,所以对于store buffer的设计中增加一个策略叫做store forwarding。就是说cpu在读取数据的时候会先查看store buffer,如果store buffer中有数据,直接用store buffer中的。这样,也就避免了使用错误数据的问题了。
store forwarding可以解决在单线程中的数据不一致问题,但是store buffer所带来的复杂性远不止如此。在多线程环境下,会有其他的问题。下面是模拟代码

public void set(){
  a=1;
  b=1;
}
public void print(){
 while(b==0)
 ;
 assert a==1;
}

假设a和b的值都是0,其中b在cpu0中,a在cpu1中。cpu0执行set方法,cpu1执行print方法。

序号 cpu0的步骤(执行set) cpu1的步骤(执行print)
1 想写入a=1,但是由于a不在自身的cacheline中,向cpu1发送read invalidate消息 执行while(b==0),由于b不在自身的cacheline中,向cpu0发送read消息
2 向store buffer中写入a=1 等待cpu0响应的read response消息
3 b在自身的cacheline中,并且此时状态为M或者E,写入b=1 等待cpu0响应的read response消息
4 收到cpu1的read请求,将b=1的值用read response消息传递,同时将b所在的cacheline修改状态为s 等待cpu0响应的read response消息
5 等待cpu1的read response和invalidate ack消息 收到cpu0的read response消息,将b置为1,因此程序跳出循环
6 等待cpu1的read response和invalidate ack消息 因为a在自身的cacheline中,所以读取后进行比对。assert a==1失败。因为此时a在自身cacheline中的值还是0,而且该cacheline尚未失效
7 等待cpu1的read response和invalidate ack消息 收到cpu0发送的read invalidate消息,将a所在的cacheline设置为无效,但是 为时已晚,错误的判断结果已经产生了
8 收到cpu1响应的read response和invalidate ack消息,将store buffer中的值写入cacheline中

通过上面的例子可以看到,在多核系统中,store buffer的存在让程序的结果与我们的预期不相符合。上面的程序中,由于store buffer的存在,所以在cacheline中的操作顺序实际上先b=1然后a=1。就好像操作被重排序一样(重排序这个词在很多文章中都有,但是定义不详,不好理解。实际上直接理解store buffer会简单很多)。为了解决这样的问题,cpu提供了一些操作指令,来帮助我们避免这样的问题。这样的指令就是内存屏障(英文fence,也翻译叫做栅栏)。来看下面的代码

public void set(){
  a=1;
  smp_mb()
首页 上一页 1 2 3 4 下一页 尾页 1/4/4
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇SpringBoot | 第二十二章:定时任.. 下一篇SpringBoot | 第二十一章:异步开..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目