五、并发控制(1):线程的互斥

本文探讨了线程并发控制中的线程互斥问题,指出处理器的乱序执行可能导致并发错误。文章通过例子说明了如何利用原子指令如`lock add`来保证指令的顺序性、原子性和可见性,以确保多线程环境下代码的正确性。同时,介绍了x86架构下的`xchg`指令用于实现简单的互斥锁,并提供了简化版的锁算法实现。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

看线程1,load(y)前面的是和y无关的store,所以操作系统允许store和load乱序。
这就使得实际可能是,线程1先执行load(y),线程2先执行load(x),然后分别进入(if !t1) goto,t1和t2同时进入临界区,所以Peterson算法出错
Perterson算法在现代计算机上实际上是错误的


在这里插入图片描述
do_sum代码中使用了内联汇编,使得其真正循环相加,彻底阻止了编译器的优化。
在这里插入图片描述
如果我们的状态机在每一个时刻,确实是执行一个线程的一条汇编指令的话,我们应该看到正确的值4n
在这里插入图片描述
这个结果就推翻了上面写的那个上节课的假设(状态机每一时刻执行一条指令),而要改为这节课的假设——指令每个时刻只能执行一个load/store或者一些线程本地的计算,如果一个x86的指令里面包含超过一个load/store,那么就会拆成若干条指令执行,每一个指令最多只能load一次或者store一次
那么进入结果的解释:

一条add指令拆分成 t=load(x);t++;store(x,t),这样的话结果就位于区间(n,4n]之间了,但是为什么也有小于n的情况。

下面进入进一步的解释:

每一个处理器都有一个写队列store buffer,每次对x的store,不会立即写入内存,而是首先写入store buffer,运行时,那个队列写入x=1,x=2,x=3…。而我们在load的时候,只有当buffer里没有x时,才会去cpu 外面读,若存在buffer中直接从buffer中读取。store buffer里的数值不是立即写入内存,而是按照1,2,3…这个写入的顺序写入内存。(例如x=1,实际上会写出cpu )。而写出cpu不是立即到达共享的内存中,而是到达cpu的缓存中。如果这时线程1cpu1上已经完成了x=1,x=2,x=3的写入,但是有可能所有的数值都还留在store buffer中,当我们的线程2在另一个cpu上执行load,会去共享内存中读取,线程2会读到sum=0,所以在线程1上完成了3次sum++的操作,但是在线程2看不到这个操作,因此sum<n就很合理了。

在这里插入图片描述
多处理器设计这么复杂完全是为了单线程执行更快而设计的,像store buffercache,但是这就让多线程的运行难以理解。


上一节课中,状态机每一步执行一个指令的假设被推翻。一条指令会分解成若干条更小的指令,处理器的乱序会得到奇怪的结果。
在这里插入图片描述
如果硬件提供一点点更多的支持,在多处理器上实现互斥就变得非常容易。


在这里插入图片描述
如果硬件能提供一些指令帮我们就好了。
在这里插入图片描述

刚才提到,在共享内存上实现互斥,只能使用load,store,和线程本地计算。而load和store本身是存在能力上的缺陷的。
例如load环顾四周,只能看不能写,而且每次只能看内存里的一个地址
store在改变物理世界状态的时候,不能读,只能把眼睛蒙起来动手。这时候这个动手成功没有,在动手的期间有没有别的线程再动过手,这都是不知道的。
再加上现代多处理器上load和store执行时可能乱序,就更复杂了。

上面那个是有着明显错误的锁实现,如果状态[IP1,IP2,locked]=[2,2,0](即if判断成功进入了那个if,这个时候后面可以同时进入临界区。


在这里插入图片描述
如果能保证load和store这中间不被打断,就不会出现[2,2,0]的情况,就可以保证这段程序的正确性。
这种指令叫做:test-and-set

t=load(locked);
if (t == 0) store(locked,1);

实际上我们的硬件我们提供了很多的原子指令.

在这里插入图片描述

能保证原子性(这样一条指令不会被打断)
能保证顺序以及多处理器间的可见性
能保证在原子指令之前所有的store会在其他的处理器之间可见,以及这样的一条原子指令能保证在这个指令前后的load和store不能被乱序,所以保证了内存访问的顺序

在这里插入图片描述
上述代码在addq上添加了一个lock
在这里插入图片描述
和前面相比,就多了一个lock,然后指令开头多了一个f0,表示这个指令需要锁定,我们的CPU在访问这条指令的时候就可以保证原子性,顺序性和可见性,因此运行时的到了正确的结果4n。
命令time ./a.out,得到这个运行的结果是0.422s
而之前那个错误的指令是0.043s,在原子指令调用比较密集的情况下,可能会有10倍甚至更多的性能差距


为了实现互斥锁,x86给我们提供了xchg指令(英文原单词应该是exchange交换).
在这里插入图片描述
xchg将传入的内存地址addr的值和传入的参数newval的值做交换,他将瞬间完成*addr 和 newval数值的交换,不会被其他处理器打断,原子完成
在这里插入图片描述
把线程想象成人,物理世界想象为共享内存,看上面的ppt,非常清楚。于是我们可以实现os课上的地一个锁算法——自旋锁

所谓的自旋spin来自量子力学,量子不断地旋转,交换交换交换
直到换到了一把钥匙,进入,所以成为自旋锁。

在这里插入图片描述
可以改写成一段非常精简的代码:
可以把tabel重命名为lockedKey=0NOTE=1
改写成->:

while (1){
	int ret = xchg(locked,1);
	if (ret == 0) break;
}

可以进一步改写成:

while( xchg(locked,1) ) ;

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
对内存中x这个地址做标记,让它归属某一个CPU,表示被这个CPU盯上了,之后store x的时候检查是不是那个盯上的CPU,如果是的话那返回SUCC,如果不是的话,那就返回FAILE。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值