CPU多核同步原语

程序执行顺序

在单个core上,program order是必须遵从;但在多个core上,

原子操作

保证操作的原子性,要么操作了,要么没有操作。

缓存体系

现代CPU有多层cache缓存体系,用于提升CPU访问内存的数据的性能。
4核执行多线程时,每个核心的L1 cache可能保存了同样的数据拷贝,

内存操作顺序

指从某个角度观察到的对于内存的读和写所发生的顺序,内存操作顺序并不唯一。

内存屏障同步原语

smp_mb()、
smp_wmb()、
smp_rmb()、

引入Store Buffer

为了防止store时导致的stall问题,引入了store Buffer:store发生时,对应的cache没有就位,就先将这个store缓存住,
如果一个core 的store进入了自己的store buffer,我们就认为这个store已经完成了,因为随后这个store buffer在cache就位的时候会自动写入到对应的cache中
Store Buffer引发的问题:

a = 1;
b = 2;
assert(a == 1);

如果这个load a发生在cache存在,但是出于Share状态无法更改的时候,那么就会load到a的旧值0,导致断言失败。
然而实际上我们在程序中先将a赋值为1了,断言从程序角度来讲不应该失败(仅仅core0修改a,不涉及其他core)。因此,这违背了程序顺序,硬件必须解决这个问题。

问题出现的根源在于,此时在core 0的视角,变量a有两份,一份位于store buffer中,是core 0修改a的过程。另一份则位于cache中,是a的旧值。如果只是简单的引入store buffer缓冲写,而不考虑这两份拷贝之间的相互关系,就会出现我们之前说的错误

解决方法
解决这个错误的方法其实也很简单,我们在多级流水线CPU设计的时候就经常使用这种技术,forwarding。此时两个拷贝,显然是store buffer里保存的才是最新的数据。在这种情况下,core 0对于所有随后的load操作,必须先检查一下store buffer中是否有pending的store,如果有,那么就取对应pending的store的值,否则才可以考虑取cache中的值

所有使用store buffer的设计,必须对应实现store buffer forwarding,因为如果不实现forwarding,单核程序上的执行就不再遵从程序顺序

Memory Barrier在多线程中的应用

在CPU多核上执行代码可能出现
core0执行代码:

a = 1;
b = 2;
assert(a == 1 && b == 2);

假设a所在的cache出现了miss,因此,a的store不得不进入store buffer。之后,对于b的store命中了core 0的一条Exclusive的cache line,因此对于b的store迅速进入了cache中。
core 1 这时执行代码:

while (b != 2) {}
assert(a == 1);

core 1通过从自己的cache中通过MESI协议load到了b的最新值,所以core 1会终止循环,进入assert(a == 1)断言部分。
此时由于core 0对a的store仍然在core 0自己的store buffer中,所以core 1从cache中load到了a的旧值,导致断言失败。

可见,在单核上运行无误的代码,在多核上运行很可能出问题,其原因在于:
store buffer的引入,导致了进行store动作的core所观察到的store序列完成的顺序,与这些store最终进入global memory的顺序出现了不一致
这种不一致行为,导致了依赖于global memory order进行同步的多线程程序无法正确同步彼此之间的store状态。

解决方法
一种可行的思路是强制保证所有store的顺序在单个core之间和global memory之间的一致:

将这个store塞入store buffer中,以保证store buffer序列化作用于global memory中,保证一个core的store顺序天然地等同于这些store作用于global memory的顺序

另一种可行的思路是放任这种观察到的store order不匹配的出现,使用smp_wmb()方法:

如果core执行到write memory barrier,这个时候在store buffer中所有pending的store动作都会被mark上,直到这些被标注的store都进入到global memory之后,随后的store才能进入global memory order中。因此,write memory barrier保证了这个barrier前后的顺序。至于wmb之前部分的那些store,以及之后部分store完成的顺序是否与global memory order匹配,则不关心。

引入invalidate queue及问题

invalidate queue用于获取同时共享与多个Core的变量的修改权:如果Core 0 核心要修改变量a,那么必须发送a变量invalidate给Core 1,当Core 1向Coren 0确定a变量invalidate后,Core才获得修改权从而修改变量a。

invalidate queue问题:
Invalidate queue的引入,使得一个core可以快速返回给别的core所发出的invalidate请求,然后再在合适的时机执行invalidate操作。潜在地,这个core的load操作就可能拿到本应该被invalidte的数据,造成这个core所观察到的数据被修改的order与实际的global memory order出现了不匹配,ex:
core 0执行:

a = 1;
wmb();   // wmb()保证了core 0中的store顺序(先a后b)一定正确地匹配global memory的顺序
b = 2;

core 1执行:

while(b != 2) {}; // 当core 0 对b修改完成后,core1观察到对b的修改,就会跳出循环。而从global memory order来看b的store完成,那么a的store也肯定完成了
assert(a == 1);

由于invalidate queue的存在,core 1对于a的load很可能会命中core 1自己的一个cache line,而这个cache line本应该被invalidate queue中的一个invalidate命令标记无效的,因此,core 1获取的仍是a的旧值(出现观察到的memory order与global memory order不匹配的情况),导致断言失败

invalidate queue的存在导致core上观察到的load顺序可能与global memory order不一致,对此,我们需要使用rmb()来解决:

rmb()给我们的invalidate queue加上标记。当一个load操作发生的时候,之前的rmb()所有标记的invalidate命令必须全部执行完成,然后才可以让随后的load发生。这样,我们就在rmb()前后保证了load观察到的顺序等同于global memory order。

程序改进, core 0执行:

a = 1;
wmb(); 
b = 2;

core 1则执行:

while(b != 2) {};
rmb();   // rmb()保证load顺序同global memory order一致:即标记的invalidate命令必须全部执行完成,就不会读取到无效的变量值
assert(a == 1);

总结

wmb() memory barrier: 作用于store buffer,只保证store操作顺序与global memory order一致
rmb() memory barrier: 作用于invalidate queue,只保证load操作顺序与global memory order一致
mb(),就是rmb()和wmb()的结合,同时标记store buffer和invalidate queue。在mb()前后,所有的load/store操作都必须拥有与global memory一致的顺序

参考

CPU多核同步原语

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值