多核并发-临界区/自旋锁原理

概论

并发问题:有可能在同一时间,多个CPU同时访问了同一份内存(至少一个CPU对数据进行写操作), 就会出现并发问题

线程同步问题: 同一份内存,被多个线程访问(至少一个线程对数据进行写操作),就会出现线程同步问题。

 

临界区

线程同步问题可以通过临界区的方式实现:设置一个令牌,一个线程申请到令牌就让他执行,其他线程申请令牌时,拿不到令牌就让这个线程自己进入阻塞状态,等待系统重新唤醒线程。唤醒后,再尝试获取令牌,直到获取到令牌然后执行正常流程,最后正常退出。

 

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

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

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

 

临界区的实现

临界区机制可以保证临界区内的代码是线程互斥的,但临界区本身是怎么实现的呢?进入临界区前需要判断令牌,判断/修改令牌本身就可能出现线程同步问题。

 

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

   

Start:
    mov eax, 1
    xchg [令牌], eax
    cmp eax, 1
    jz GoToSleep
    .....
    .....
    .....
    mov [令牌], 0
    ret

GoToSleep:
    Sleep(10)
    jmp Start

 

 

“令牌”值为1表示临界区已被占用,为0表示未被占用。xchg指令会对“令牌”这块内存和eax进行交换:

  1. 当“令牌”的值为1,执行后“令牌”为1,eax为1。

  2. 当“令牌”的值为0,执行后“令牌”为1,eax为0。

当eax为0时,就可以顺利执行临界区的代码,为1就直接跳到GoToSleep。

 

把对“令牌”这块内存的读写操作都压缩到一条指令,就成功规避了线程切换问题,即使线程切换发生在“cmp eax,1”前,也不会对临界前,另一个线程也无法成功进入到临界区,而是跳到GoToSleep进入阻塞状态,等待操作系统将其唤醒。

 

多核并发问题

上面的临界区实现,在单核情况下是没有问题。但是在多核环境下就会出现并发问题:在执行“xchg [令牌],eax”指令时,如果在这一瞬间有多个CPU同时执行,那么这几个CPU里的eax可能都会是0。

 

为什么会出现这种问题呢? 这是因为上面的指令并不能保证两个CPU互斥访问一块内存。但是X86架构提供了解决方案:在“xchg”指令前添加“lock”前缀。 只要添加了“lock“前缀,就能保证一条指令在执行过程中,读取内存的操作是互斥的,是前后有序的。

 

修改后的代码如下:

   

Start:
    mov eax, 1
    lock xchg [令牌], eax
    cmp eax, 1
    jz GoToSleep
    .....
    .....
    .....
    mov [令牌], 0
    ret

GoToSleep:
    Sleep(10)
    jmp Start

 

 

自旋锁

前面介绍的临界区机制本质上是线程切换,但当我只需要对一小段代码进行互斥操作的时使用临界区就会造成浪费。

接着上面的代码稍微改一下, 就可以实现自旋锁了:

  

Start:
    mov eax, 1
    lock xchg [令牌], eax
    cmp eax, 1
    jz GoToSpin
    .....
    .....
    .....
    mov [令牌], 0
    ret

GoToSpin:
jmp Start

 

这段代码和临界区唯一的区别就是:它没有Sleep, 这意味着CPU没有主动放弃执行权。只要等到“令牌”被置

为0那么它就可以执行互斥的代码了。

 

 

本质上它就是个死循环,CPU在执行死循环过程时电力消耗比较大,所以可以优化一下,在“jmp Start”之前价格“pause”指令。“pause”指令可以让CPU等待,降低电力消耗。 优化代码如下:

Start:
    mov eax, 1
    lock xchg [令牌], eax
    cmp eax, 1
    jz GoToSpin
    .....
    .....
    .....
    mov [令牌], 0
    ret

GoToSpin:
    pause
jmp Start

  自旋锁的本质,就是在等待“令牌”时CPU死循环等待“令牌”为可用状态。自旋锁的弊端:

  1. 自旋锁显然不能用到单核状态下,因为等待(自旋)过程CPU也是被占用的,没有其他CPU来释放令牌,这个等待过程就是无意义的,就是在白等。

  2. 其他CPU执行互斥代码开始到释放“令牌”这个时间段太长就是在浪费等待的CPU,等待时间太长,例如超

过了一个线程切换的周期,那我还不如把CPU主动释放,让其他有意义的线程执行呢。

猜你喜欢

转载自www.cnblogs.com/joneyyana/p/12554089.html
今日推荐