自旋锁

多核并发时出现问题怎么解决?

自旋锁

这边涉及到了多核同步问题

先简要概述下什么是自旋锁,当一个线程拿到锁后另一个线程占用另一个核跑时也需要拿锁就会判断,然后发现已经有线程拿到锁了就直接做pause操作在那空转,然后再比较。。。如果还占着锁那就不断空转,直到拿到锁。

来看一看临界区实现

操作系统在线程切换的时,操作的粒度是指令级别。换句话说就是:线程切换是发生在两个指令中间的,不会在指令执行到一半时被打断,也就是说把读写令牌的指令压缩到一条就可以规避线程同步的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Start:
mov eax, 1
xchg [令牌], eax
cmp eax, 1
jz GoToSleep
.....
.....
.....
mov [令牌], 0
ret

GoToSleep:
Sleep(10)
jmp Start

xchg指令会对“令牌”这块内存和eax进行交换

1表示在占用0则不是

这段代码在单核的情况下完全没有问题,但是到了多核就会出现多核并发问题,因为在多核下是多个线程同时在跑,假如又多个线程同时跑xchg [令牌], eax的话,可能多个线程拿到的eax都是0

所以需要lock指令,这条指令能保证改指令在执行时是互斥的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Start:
mov eax, 1
lock xchg [令牌], eax
cmp eax, 1
jz GoToSleep
.....
.....
.....
mov [令牌], 0
ret

GoToSleep:
Sleep(10)
jmp Start

但是随之而来的另一个问题是当我仅需做少许操作时利用临界区这是一种浪费

临界区本质上是通过线程切换来实现的线程互斥的效果,这种方式有两个

  1. 线程切换本身需要消耗一定的时间,效率低。

  2. 临界区粒度太大,当我只需要对一小段代码要求互斥,这段代码可能只需要几纳秒就执行完毕了,但线程切换就可能需要20毫秒,得不偿失。
    所以需要稍作修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Start:
mov eax, 1
lock xchg [令牌], eax
cmp eax, 1
jz GoToSpin
.....
.....
.....
mov [令牌], 0
ret

GoToSpin:
pause
jmp Start

这边没用用到sleep cpu就不会主动放弃执行权,等到令牌变为0就可以执行了

本质上就是个死循环,和名字一样,b自旋i,但是cpu死循环电力消耗大,所以改成pause指令可以降低电力消耗

最后来讲讲弊端

第一是只能在多核中,如果在单核中跑的话没意义,因为没有线程来恢复令牌

还有就是适合小操作,如果操作需要耗时太多超过了线程切换周期,与其用自旋锁不如直接sleep让其他线程去跑

这边让我想起了ssdthook里面需要修改cr0寄存器来改变内存页属性,当时存在核切换情况,那时还不懂以为可以用自旋锁,现在学了才发现并没有什么关系,自旋锁是为了解决多核中线程安全问题而出现的,而我需要的是防止核切换,因为一个核有一套寄存器,所以仅需使用一些特定api即可

如SetThreadAffinityMask。

//当然自旋锁从某种角度上来说也能处理,但是不怎么优雅因为要阻塞线程

文章目录
  1. 1. 自旋锁
|