无锁队列

来源:互联网 发布:淘宝怎么设置客服接待 编辑:程序博客网 时间:2024/06/09 12:28

在开始说无锁队列之前,我们需要知道一个很重要的技术就是CAS操作——Compare & Set,或是 Compare & Swap,现在几乎所有的CPU指令都支持CAS的原子操作,X86下对应的是 CMPXCHG 汇编指令。有了这个原子操作,我们就可以用其来实现各种无锁(lock free)的数据结构。

这个操作用C语言来描述就是下面这个样子:(代码来自Wikipedia的Compare And Swap词条)意思就是说,看一看内存*reg里的值是不是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;}

这个操作可以变种为返回bool值的形式(返回 bool值的好处在于,可以调用者知道有没有更新成功):

bool compare_and_swap (int *accum, int *dest, int newval){  if ( *accum == *dest ) {      *dest = newval;      return true;  }  return false;}

与CAS相似的还有下面的原子操作:(这些东西大家自己看Wikipedia吧)

  • Fetch And Add,一般用来对变量做 +1 的原子操作
  • Test-and-set,写值到某个内存位置并传回其旧值。汇编指令BST

  • Test and Test-and-set,用来低低Test-and-Set的资源争夺情况

3) C++11中的CAS

C++11中的STL中的atomic类的函数可以让你跨平台。(完整的C++11的原子操作可参看 Atomic Operation Library)

template< class T >bool atomic_compare_exchange_weak( std::atomic* obj,                                   T* expected, T desired );template< class T >bool atomic_compare_exchange_weak( volatile std::atomic* obj,                                   T* expected, T desired );

我们先来看一下进队列用CAS实现的方式:

EnQueue(x) //进队列{    //准备新加入的结点数据    q = new record();    q->value = x;    q->next = NULL;    do {        p = tail; //取链表尾指针的快照    } while( CAS(p->next, NULL, q) != TRUE); //如果没有把结点链在尾指针上,再试    CAS(tail, p, q); //置尾结点}

我们可以看到,程序中的那个 do- while 的 Re-Try-Loop。就是说,很有可能我在准备在队列尾加入结点时,别的线程已经加成功了,于是tail指针就变了,于是我的CAS返回了false,于是程序再试,直到试成功为止。这个很像我们的抢电话热线的不停重播的情况。

你会看到,为什么我们的“置尾结点”的操作(第12行)不判断是否成功,因为:

  1. 如果有一个线程T1,它的while中的CAS如果成功的话,那么其它所有的 随后线程的CAS都会失败,然后就会再循环,
  2. 此时,如果T1 线程还没有更新tail指针,其它的线程继续失败,因为tail->next不是NULL了。
  3. 直到T1线程更新完tail指针,于是其它的线程中的某个线程就可以得到新的tail指针,继续往下走了。

这里有一个潜在的问题——如果T1线程在用CAS更新tail指针的之前,线程停掉或是挂掉了,那么其它线程就进入死循环了。下面是改良版的EnQueue()

EnQueue(x) //进队列改良版{    q = new record();    q->value = x;    q->next = NULL;    p = tail;    oldp = p    do {        while (p->next != NULL)            p = p->next;    } while( CAS(p.next, NULL, q) != TRUE); //如果没有把结点链在尾上,再试    CAS(tail, oldp, q); //置尾结点}

我们让每个线程,自己fetch 指针 p 到链表尾。但是这样的fetch会很影响性能。而通实际情况看下来,99.9%的情况不会有线程停转的情况,所以,更好的做法是,你可以接合上述的这两个版本,如果retry的次数超了一个值的话(比如说3次),那么,就自己fetch指针。

好了,我们解决了EnQueue,我们再来看看DeQueue的代码:(很简单,我就不解释了)

DeQueue() //出队列{    do{        p = head;        if (p->next == NULL){            return ERR_EMPTY_QUEUE;        }    while( CAS(head, p, p->next) != TRUE );    return p->next->value;}

我们可以看到,DeQueue的代码操作的是 head->next,而不是head本身。这样考虑是因为一个边界条件,我们需要一个dummy的头指针来解决链表中如果只有一个元素,head和tail都指向同一个结点的问题,这样EnQueue和DeQueue要互相排斥了

注:上图的tail正处于更新之前的装态。