zoukankan      html  css  js  c++  java
  • 无锁编程本质论

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

    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和内存屏障(这些都会涉及机器架构或编译器)而已,并不是真正的无锁。。。

     

  • 相关阅读:
    docker数据卷
    docker容器的启动、停止、运行、导入、导出、删除
    docker镜像的获取、创建、修改、删除、导入操作
    docker使用-spark安装
    python爬虫-3 解析库
    python爬虫-2 requests使用
    NLP-HMM
    NLP-中文分词-预处理
    python爬虫-1环境安装
    学习笔记3
  • 原文地址:https://www.cnblogs.com/kaleidoscope/p/9588282.html
Copyright © 2011-2022 走看看