CAS无锁队列的原理及实现(附代码)

简介

    在进入今天的主题之前,我们先来了解一下一般使用的比较常用的锁。互斥锁和自旋锁。

    互斥锁:如果取不到锁就会进入休眠,本身取锁的操作并不耗时,主要就是等待拿到锁的时间,并且这样的话会进行线程切换,比较耗资源;自旋锁就不一样了,在没有获取到锁的情况下不会休眠,而是一直忙等待下去,一直占据CPU,不进行线程的切换,这样的好处就是执行本身耗时比较短的操作时,加锁的代价比较小,但是如果本身加锁执行的操作很长或者会有可能阻塞的话,其他锁一直忙等也是很耗CPU的。

    首先需要先了解一下CAS无锁队列的概念。CAS的意思是Compare And Swap,从字面意思上面也可以知道实际就是对数据进行交换的一种原子操作。对应到CPU指令的话就是cmpxchg。
    无锁队列的内部实现实际也是原子操作,可以避免多线程调用出现的不可预知的情况。主要的核心就是函数__sync_bool_compare_and_swap,返回值为bool型,原子交换操作成功返回true,失败返回false。

    在了解无锁队列之前,我们先了解一下什么是原子操作,原子操作通俗的将就是一段指令的执行期间不会被其他的进程或者线程打断,可以保证执行的一致性。举个简单的反例,i++的过程中实际上就不是一个原子操作。

    i++内部存在三个步骤:

        ① 先将i的值从内存中读到寄存器;

        ② 将寄存器中的i的值加1;

        ③ 将寄存器的值读回内存中。

    对于原子操作本身gcc也封装了一套接口,c++也有一套自己的原子操作的接口可以使用。从原理上来讲,原子操作是在使用Lock#指令前缀的时候,操作执行期间对count内存的并发访问被禁止,保证操作的原子性。

                                                 

代码实现

    介绍完原子操作,接下来就简单的看一下CAS的实现。CAS主要有三个操作数,当前值A、内存值V和要更改的新值B。当当前值A跟内存值V相等,那么就将内存值V改成B;当当前值A和内存值V不想等的话,要么就重试,要么放弃更新B。实际就是多线程操作的时候,不加锁,多线程操作了共享的资源之后,在实际修改的时候判断是否修改成功。

    而且,CAS会有个ABA的问题。简单的说就是线程A将当前值修改为10,此时线程B将值改为11,然后又有一个线程C把值又改为10,这样的话对于线程A来说取到的内存值和当前值是没变的,所以可以更新,但实际上是经过变化的,所以不符合实际逻辑的。想要解决这个问题的话就需要加一个版本,Java中有AtomicStampedReference类可以添加版本在比对内存值的时候加以区分。

    以下是一个简单的无锁队列的代码:

// 定义一个链表实现队列
template <typename ElemType>
struct qnode // 链表节点
{
  struct qnode *_next;
  ElemType _data;
};

template <typename ElemType>
class Queue
{
private:
  struct qnode<ElemType> *volatile _head = NULL;  // 随着pop后指向的位置是不一样的, head不是固定的
  struct qnode<ElemType> *volatile _tail = NULL;

public:
  Queue()
  {
    _head = _tail = new qnode<ElemType>;
    _head->_next = NULL;
    _tail->_next = NULL;
    printf("Queue _head:%p\n", _head);
  }

  void push_list(const ElemType& e) {
    struct qnode<ElemType>* p = new qnode<ElemType>;
    if (!p) {
      return ;
    }
    p->next = NULL;
    p->data = e;
    
    struct qnode<ElemType>* t = _tail;
    struct qnode<ElemType>* old_t = _tail;
    
    do {
      while (t->next != NULL) {  // 当非NULL的时候说明不是尾部,因此需要指向下一个节点
        t = t->next;
      }
    // 如果t->next为则null换为p
    } while (!__sync_bool_compare_and_swap(&t->next, NULL, p));
    // 如果尾部和原来的尾部相等,则换为p。
    __sync_bool_compare_and_swap(&_tail, old_t, p);
  }
  
  bool pop_list(ElemType& e) {
    struct qnode<ElemType>* p = NULL;
    do {
      p = _head;
      if (p->next == NULL) {
        return false;
      }
    // 如果头部等于p,那么就指向p的下一个
    } while (!__sync_bool_compare_and_swap(&_head, p, p->next));

    e = p->_next->data;
    delete p;
    p = NULL;
    return true;
  }
};

    以上就是无锁队列的实现,首先定义一个链表,链表的头结点没有存放数据,在push数据的时候,先判断当前指针是否是在队列的尾部,如果是的话就使用CAS判断是不是走到了尾部,如果是的话就将NULL节点更新为P,同时后面需要将尾节点更新为P;同样在pop的时候同样也是在循环中判断头部指向下一个,然后之前的节点可以删除。

结论:

    无锁队列的原理内部还是原子操作的比较,真返回true,假返回false。无所队列在ZeroMQ等中间件中都有所应用。我们比较熟悉的mutex应用最广泛,他的原理是线程没有获取到锁的话会就如休眠等待,让出CPU,上下文切换,效率较低。在释放锁的时候会先检查owner是否是自己,如果是自己的话就会在休眠的队列中取出一个task,唤醒task去执行。一般的应用场景是共享区域执行的任务较长的时候。上面的队列也可以使用有锁的队列实现,具体就是利用mutex以及c++自带的STL,list来实现的。spinLock也是常见的一种锁,他的特点是线程在没有获取锁之前会进入忙等待,不会让出CPU执行,不会进程线程切换。一般应用在执行任务不会阻塞、耗时短、执行任务简单的场景,这种锁比较耗资源,需要谨慎考虑使用的场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值