关注公众号获取更多信息:
今天继续聊内存模型。
上篇文章讲了一些概念。今天讲一些小细节,为最后的总结做铺垫。
上图所示的是一个典型的多核CPU系统架构,它包含有2个CPU核,每个CPU核有一个私有的32KB 的 L1 cache,两个CPU 核共享 1MB的 L2 cache 以及 512MB的主存。
在这个内存模型下,cpu写数据并不是立即写入RAM中,而是写入L1 cache,再从L1 cache存入(store) RAM中,读数据也是先从L1 cache中读,读不到再从RAM中读,这种读写数据的模式是能够提高数据存取效率的,但是在一些特殊情况下会导致程序出错,考虑以下这个例子。
x=y=0; Thread 1 Thread 2 x = 1; y = 1; r1 = y; r2 = x;
表面上看,r1==r2==0这种输出是不可能出现的,然而,有一种可能性是,由于r1不依赖于x,编译器可以把r1=y这步操作调整到x=1这步操作之前,同样,r2=x这步操作可以调整到y=1这步操作之前,这样一来,core 1可以先读取L1 cache中的y的值,core 2 才执行 y = 1的赋值操作,同理,r2 = x这步操作也可以在x=1这步赋值操作之前执行,这时候就会出现r1 == r2 ==0的输出结果。
为什么会出现这种情况呢?
从软件层面来说,这个锅是编译器的
编译器了解底层CPU的思维模式,因此,它可以在将c翻译成汇编的时候进行优化(例如内存访问指令的重新排序),让产出的汇编指令在CPU上运行的时候更快。然而,这种优化产出的结果未必符合程序员原始的逻辑。
从CPU层面来说
在运行时,CPU 会乱序执行指令,乱序处理器(Out-of-order processors)处理指令通常有以下几步:
-
指令获取
-
指令被分发到指令队列
-
指令在指令队列中等待,直到输入操作对象可用(一旦输入操作对象可用,指令就可以离开队列,即便更早的指令未被执行)
-
指令被分配到适当的功能单元并执行
-
执行结果被放入队列(而不立即写入寄存器堆)
-
只有所有更早请求执行的指令的执行结果被写入寄存器堆后,指令执行的结果才被写入寄存器堆(执行结果重排序,让执行看起来是有序的)
从上面的执行过程可以看出,乱序执行相比有序执行能够避免等待不可用的操作对象(有序执行的第二步)从而提高了效率。现代的机器上,处理器运行的速度比内存快很多,有序处理器花在等待可用数据的时间里已经可以处理大量指令了。
我们先不考虑具体的软件或具体的硬件情况,仅仅从逻辑上考虑这个问题。
最简单的模型就是只简化内存操作为Store(内存存储)和Load(内存读取)这两个操作,因此2×2可以组合出至少以下4种序列。
-
Store, Load (SL)
-
Store, Store (SS)
-
Load, Load (LL)
-
Load, Store (LS)
如果某个barrier可以保证Store,Load不被乱序执行,则我们称其为SL memory barrier(SLB)。类似的我们可以定义出SSB, LLB, LSB。
举个例子,为了保证执行顺序,我们可以插入一个SSB。
int port, action;
void forbar(int base)
{
action = 4; __asm__("SSB"); port = base + 3;
}
这样我们就可以保证两个store是按顺序执行的。
c++11定义的模型比这个要更复杂,大概有以下
-
full memory barrier(mb)
-
release memory barrier(relb)
-
acquire memory barrier(acqb)
-
write memory barrier(wb)
-
read memory barrier(rb)
-
data dependency memory barrier(ddrb)
为啥不是2*2=4种情况呢?
这就涉及到Acquire与Release语义
Acquire与Release是无锁编程中最容易混淆的两个原语,它们是线程之间合作进行数据操作的关键步骤。在这里,借助前面对memory barrier的解释,对acquire与release的语义进行阐述。
-
acquire本质上是read-acquire,它只能应用在从RAM中read数据这种操作上,它确保了所有在acquire之后的语句不会被调整到它之前执行,如下图所示。
用上面的memory barrier来描述,acquire等价于LoadLoad加上LoadStore栅栏。
-
release本质上是write-release,它只能应用在write数据到RAM中,它确保了所有在release之前的语句不会被调整到它之后执行,如下图所示。
用上面的memory barrier来描述,release等价于LoadStore加上StoreStore栅栏。
发现没有,现在情况增加了一个维度,就是上边四种之前和之后。
比如relb这个的语义是release write memory barrier,要保护的序列是“anymemops, relb, action”这种序列,这个anymemops可以是多个store和load的任意组合,可以保证在执行关键操作前一定可以得到最新的内存信息。
而acqb就是acquire load memory barrier,要保护的序列是“action, acqb, anymemops”,可以保证执行关键操作后的anymemops是基于最新的内存数据。
顺便提一句,这个action往往就是各种atomic operation,但并非一定是。这也是memory barrier容易与原子操作混淆的一个原因。
好了就到这里,下次讲C++11的六种内存模型