mylinuxer

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

转载 http://weibo.com/p/1001603876869958445266

无锁编程本质论

2015年8月17日 16:48 阅读 9273
作者:新浪微博(@NP等不等于P

计算机学习微信公众号(jsj_xx)

无锁编程真的是不涉及锁么?无锁编程实现的本质是什么?需要操作系统或者编译器的支持么?本文尝试解答这些问题。

1 锁引发的问题

使用锁时要特别防止出现死锁或活锁。死锁的情况很简单,就是申请者在申请过程中由于顺序原因(多个锁没有按固定顺序申请)进入堵塞状态了,指定顺序即可规避。我们只看一个活锁的例子:

 

两个线程都在尽量避免死锁,但是却有可能(尴尬地)进入了活锁场景:两个线程都不会如期进入do_something_x(),也都不会退出while(),而是处于反复尝试过程里!可见,活锁的含义就是申请者都活着(没有堵塞住)但还是锁住了。

上面活锁的例子,其实和下面这个(不使用任何锁的)例子是一个意思:

 

不考虑cpu/compiler优化的情况下,如果两个线程都在运行这个while(),则有可能两个线程都退不出这个while()。因为两个线程都在修改X,有可能造成while(X==0)的X一直为0!

2 无锁实例

无锁编程的本质就是处理最关键的点(一般针对最精简的数据结构里面的某个字段),所谓“好钢用在刀刃上”:使用CAS原子操作方式将有锁操作压缩到最小范围(可见无锁的本质还是有锁的,原子操作也是锁性质的),CAS原子操作会封装成下面的形式:(以32bit机为例)

bool cas32( int * pVal, int oldVal, int newVal );

pVal表示要要考察值的地址,oldVal表示期望的旧值,newVal表示可以替换时的新值。此函数相当于:

int compare_and_swap (int* reg, int oldval, int newval) 
{   
     int old_reg_val = *reg;   
     if (old_reg_val == oldval)      
              *reg = newval;   
     return old_reg_val; 
}

这样,无锁相当于把锁因素转化为原子CAS操作而压缩到了最精简的数据结构,以至于看上去貌似无锁!我们看一个queue的无锁化:(本实例来自1994年的《Implementing Lock-Free Queues》,代码中“^.”相当于“->”)

搞清这个无锁queue,需要理解几个要点:

  • Queue始终含有一个dummy冗余节点,包括初始时。在后续的删除过程中,这个dummy节点会随时切换(指向旧节点)。

  • 对于EnQueue(),第一个CAS确保第一个取到tail的操作者顺利加入队列;第二个CAS让没有顺利加入时(会反复尝试)能够快速推进tail指针的更新;第三个CAS很显然,第一个顺利完成入队列操作自然需要完成tail指针的更新,虽然有可能已经被第二个CAS提前更新过了。

  • 对于Dequeue(),唯一的CAS是确保第一个取到head值的操作者顺利删除队列第一个节点。

总之,在EnQueue和DeQueue已有支持多线程特性基础(入队列和出队列在队列中至少一个节点的情况下,是不需要加锁保护的)上,通过CAS的使用,EnQueue()和DeQueue()各自都分别能支持多线程了。这样,看上去很美:无锁!

另外,需要注意异常情况,比如上面的EnQueue()如果在执行过程中某个线程挂掉,是否影响其它线程?如果删掉里面第二个CAS操作,那么EnQueue()就会有问题:thread1成功完成第一个CAS,开始第三个CAS(因为删除了第二个CAS,其实此时应该是第二个了)时挂掉了,那么其它thread都会死循环:死等next字段为NUL的tail,但其实这个tail的next永远停留在thread1刚入队列的那个节点了。

以上可见,EnQueue()里第二个CAS很重要,但防止上述某个线程中途崩溃的方法还有其它途径,比如这个EnQueue()版本:

  

处理原则都一样,就是:即使CAS没能顺利成功也需要推进tail指针!

3 gcc中的无锁支持

bool __sync_bool_compare_and_swap(type *ptr, type oldval type newval, ...)

type __sync_val_compare_and_swap(type *ptr, type oldval type newval, ...)

These builtins perform an atomic compare and swap. That is, ifthe current value of *ptr is oldval, thenwrite newval into *ptr.

The “bool” version returns true if the comparison is successfuland newval was written. The “val” version returns the contentsof *ptr before the operation.

我们仔细看下__sync_bool_compare_and_swap():

我们聚焦cmpxchg:

cmpxchg %ecx, %ebx;如果EAX与EBX相等则ECX送EBX且ZF置1;否则EBX送EAX且ZF清0

%eax是旧值,%ecx就是新值,%ebx就是待考察的值。原来,这条指令就是无锁根源!几乎所有CPU都支持CAS原子操作,x86下对应的原子操作就是cmpxchg指令。

4 CAS的ABA问题

ABA问题是指由于进程切换导致状态变化的遗漏。比如进程看到的变量是“A”,但其实可能已经经过多次状态变化了:“A->B->A”。

解决办法一般是增加一个引用计数字段。这样,CAS同时检查引用计数和目标内容两个值是否都没有发生变化。但麻烦的是引用计数字段也有一个溢出问题。
5 内核中的无锁

查看内核kfifo机制可看到,内核采用另一种无锁方式:内存屏障。关于内存屏障话题,具体请参考我们之前的《理解内存屏障》五部曲。

gcc中也对内存屏障有封装:

__sync_synchronize (...)

This builtin issues a full memory barrier.

6 总结

可见,无锁的本质是使用CAS和内存屏障(这些都会涉及机器架构或编译器)而已,并不是真正的无锁。。。

关于我们

新浪微博(@NP等不等于P

计算机学习微信公众号(jsj_xx)

原创技术文章,感悟计算机,透彻理解计算机!

 

posted on 2016-01-26 10:18  mylinuxer  阅读(1948)  评论(0编辑  收藏  举报