1.内存模型
1.1 概念
为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了 CPU 多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。
1.2 内存模型解决并发问题主要采用两种方式:
限制处理器优化
使用内存屏障
仅仅阅读内存模型的概念可能有点摸不着头脑,举个实际例子,随着硬件的不断升级,计算机处理逻辑会遇到很多问题,例如:缓存一致性问题、处理器器优化的指令重排问题等。
2.CPU 和缓存一致性问题
我们都知道 CPU 和 内存是计算机中比较核心的两个东西,它们之间会频繁的交互,随着 CPU 发展越来越快,内存的读写的速度远远不如 CPU 的处理速度,所以 CPU 厂商在 CPU 上加了一个 高速缓存,用来缓解这种问题。
下图是小编用 CPU-Z 查看的本机硬件参数
一般高速缓存有 3 级:L1,L2,L3,CPU 与内存的交互,就发生了变化,CPU 不再与内存直接交互,CPU 会先去 L1 中寻找数据,没有的话,再去 L2 中寻找,然后是 L3,最后才去内存寻找(更准确的来说,应该是 CPU 中的寄存器去寻找)。我们可以画一张图来理解:
看起来一切都很美好,但是随着科技的进步,CPU 厂商们叒搞事了,推出了多核 CPU,每个 CPU 上又有高速缓存,CPU 与内存的交互就变成了下面这个样子:
2.1 缓存不一致问题
为什么会出现这个问题呢? CPU 需要修改某个数据,是先去 Cache 中找,如果 Cache 中没有找到,会去内存中找,然后把数据复制到 Cache 中,下次就不需要再去内存中寻找了,然后进行修改操作。
而修改操作的过程是这样的:在 Cache 里面修改数据,然后再把数据刷新到主内存。其他 CPU 需要读取数据,也是先去 Cache 中去寻找,如果找到了就不会去内存找了。
所以当两个 CPU 的 Cache 同时都拥有某个数据,其中一个 CPU 修改了数据,另外一个 CPU 是无感知的,并不知道这个数据已经不是最新的了,它要读取数据还是从自己的 Cache 中读取,这样就导致了“缓存不一致”。
2.2 解决缓存不一致的方法
解决缓存不一致的方法有很多,比如: 总线加锁(此方法性能较低,现在已经不会再使用)MESI 协议: 当一个 CPU 修改了 Cache 中的数据,会通知其他缓存了这个数据的 CPU,其他 CPU 会把 Cache 中这份数据的 Cache Line 置为无效,要读取数据的话,直接去内存中获取,不会再从 Cache 中获取了。
当然还有其他的解决方案,MESI 协议是其中比较出名的。
★
MESI(Modified Exclusive Share Invalid)(也称伊利诺斯协议)是一种广泛使用的支持写回策略的缓存一致性协议,该协议被应用在 Intel 奔腾系列的 CPU 中。
”
MESI 协议中的状态
CPU 中每个缓存行使用的 4 种状态进行标记(使用额外的两位 bit 表示)
状态
描述
M(Modified)
这行数据有效,数据被修改了,和内存中的数据不一样,数据只存在于本 cache 中
E(Exclusive)
这行数据有效,数据和内存中的数据一致,数据只存下于本 Cache 中
S(Shared)
这行数据有效,数据和内存中的数据一致,数据存在于很多 cache 中
I(Invalid)
这行数据无效
M 和 E 的数据都是本 core 独有的,不同之处是 M 状态的数据是 dirty(和内存中的不一致),E 状态的数据是 clean(和内存中的一致)
S 状态是所有 Core 的数据都是共享的,只有 clean 的数据才能被多个 core 共享
I-表示这个 Cache line 无效
E 状态
只有 Core 0 访问变量 x,它的 Cache line 状态为 E(Exclusive)。
S 状态
3 个 Core 都访问变量 x,它们对应的 Cache line 为 S(Shared)状态。
M 状态和状态之间的转化
Core 0 修改了 x 的值之后,这个 Cache line 变成了 M(Modified)状态,其他 Core 对应的 Cache line 变成了 I(Invalid)状态
在 MESI 协议中,每个 Cache 的 Cache 控制器不仅知道自己的读写操作,而且也监听(snoop)其它 Cache 的读写操作。每个 Cache line 所处的状态根据本核和其它核的读写操作在 4 个状态间进行迁移
★
MESI 协议通过标识缓存数据的状态,来决定 CPU 何时把缓存的数据写入到内存,何时从缓存读取数据,何时从内存读取数据。
”
3.处理器优化问题
3.1 处理器优化问题
MESI 协议看似解决了缓存的一致性问题,但是并不那么完美,因为当多个缓存对数据进行了缓存时,一个缓存对数据进行修改需要同过指令的形式与其他 CPU 进行通讯,这个过程是同步的,必须其他 CPU 都把缓存里的数据都置为 Invalid 状态成功后,我们修改数据的 CPU 才能进行下一步指令,整个过程中需要同步的和多个缓存通讯,这个过程是不稳定的,容易产生问题,而且通讯的过程中 CPU 是必须处于等待的状态,那么也影响着 CPU 的性能。
3.2 处理器优化解决方案
为了避免这种 CPU 运算能力的浪费,解决 CPU 切换状态阻塞,Store Bufferes 被引入使用。
处理器把它想要写入到主存的值写到缓存,然后继续去处理其他事情。当所有失效确认(Invalidate Acknowledge)都接收到时,数据才会最终被提交。
4.指令重排问题
处理器优化会导致指令重排序问题
value = 3;
void 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 会读到跟程序中写入的顺序不一样的结果。
指令重排序解决方案:
硬件工程师其无法预知未知的程序逻辑场景,所以一些问题还是遗留给了软件工程师,但是他们给我们提供了一套对应场景的解决方案就是“内存屏障指令”,我们的软件工程师可以同内存屏障来针对不同场景来选择性的“禁用缓存”。
内存屏障分为下面几种:
lfence(读屏障 load Barrier):在读取指令前插入读屏障,让缓存中的数据失效,重新从主内存加载数据,保证数据是最新的。
Sfence(写屏障 store Barrier): 在写入指令后插入屏障,同步把缓存的数据写回内存,保证其数据立即对其他缓存可见。
Mfence(全能屏障):拥有读屏障和写屏障的功能;
Lock 前缀指令:拥有类似全能屏障的功能。
void executedOnCpu0() {
value = 10;
//在更新数据之前必须将所有存储缓存(store buffer)中的指令执行完毕。
storeMemoryBarrier();
finished = true;
}
void executedOnCpu1() {
while(!finished);
//在读取之前将所有失效队列中关于该数据的指令执行完毕。
loadMemoryBarrier();
assert value == 10;
}
5.总结:
随着计算机高速发展,CPU 技术远超过内存技术,所以多级缓存被使用,解决了内存和 cpu 的读写速度问题,随着多线程的发展,缓存一致性问题油然而生,好在可以通过缓存一致性协议来解决,比较出名的缓存一致性协议是MESI,MESI协议的引入,微微降低了 cpu 的速度。
为了更好的压榨 cpu 的性能,于是Store Bufferes概念被引入,将 cpu 写入主存从同步阻塞变为异步,大大提高了 cpu 执行效率
喜闻乐见,指令重排序问题预期而至,这时候祭出终极武器:内存屏障指令,在代码里面禁用缓存。
至此,计算机发展中遇到的问题都一一解决,而这一系列问题解决方案,都是内存模型规范的。
内存模型就是为了解决计算机发展中遇到的缓存一致性、处理器优化和指令重排、并发编程等问题的一系列规范,他定义了共享内存系统中多线程程序读写操作行为的规范,通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。