无锁队列(一)

一、数据结构

Head,Tail是队列的头和尾。

二、基础知识

一般的处理器,有一条指令,一个周期就可以执行,也可以说是原子操作,是不可分割的。

CAS操作——Compare & Set,或是 Compare & Swap,现在几乎所有的CPU指令都支持CAS的原子操作,X86下对应的

是 CMPXCHG 汇编指令。有了这个原子操作,我们就可以用其来实现各种无锁(lock free)的数据结构。

利用c语言描述其功能:

bool compare_and_swap (int* accum,int* dest,int newval)
{
  if( *accum == *dest ) {
      *dest = newval;
      return true;
  }
  return false;
}
还有一些类似的指令:

  • Fetch And Add,一般用来对变量做 +1 的原子操作
  • Test-and-set,写值到某个内存位置并传回其旧值。汇编指令BST
  • Test and Test-and-set,用来低低Test-and-Set的资源争夺情况
三、实现

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


 分析:

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

  1. 如果有一个线程T1,它的while中的CAS如果成功的话,那么其它所有的 随后线程的CAS都会失败(此时其它线程的p->next!=null),然后就会再循环,
  2. 此时,如果T1 线程还没有更新tail指针(更新之前,tail指针是固定的),其它的线程继续失败,因为tail->next不是NULL了。
  3. 直到T1线程更新完tail指针,于是其它的线程中的某个线程就可以得到新的tail指针,继续往下走了。
重点在于,如果T1在  CAS(tail, p, q); //置尾结点(6) 处挂点了。此时其它线程在 while( CAS(p->next, NULL, q) != TRUE)中出不来。代码在死循环中。

EnQueue(x)//进队列改良版
{
    q = new record();
    q->value = x;
    q->next = NULL;

// 代码中fifo的最后一个元素为tail
    p = tail;
    oldp = p
    do{
        while(p->next != NULL)
            p = p->next;
    }while( CAS(p->next, NULL, q) != TRUE); //如果没有把结点链在尾上,再试
 
    CAS(tail, oldp, q); //置尾结点
}


在每个线程中,让自己的链表的尾节点做 CAS,这样,即使上一个版本在(6)挂掉了,也没有关系。

--------------------------------------------------------------------------------------------------------------

int DeQueue()//出队列
{
    do{
        p = head;
        if(p->next == NULL){
            returnERR_EMPTY_QUEUE;
        }
    while( CAS(head, p, p->next) != TRUE );
    return(p->next->value);
}
//个人理解上面的代码有错误
// head 应该为fifo第一个元素
int DeQueue()//出队列
{
    do{
        p = head;
        if(p == NULL)
       {
            return ERR_EMPTY_QUEUE;
        }
    while( CAS(head, p, p->next) != TRUE );
    return(p->next->value);
}



四、现实的问题

CAS的ABA问题

所谓ABA(见维基百科的ABA词条),问题基本是这个样子:

  1. 进程P1在共享变量中读到值为A
  2. P1被抢占了,进程P2执行
  3. P2把共享变量里的值从A改成了B,再改回到A,此时被P1抢占。
  4. P1回来看到共享变量里的值没有被改变,于是继续执行。

虽然P1以为变量值没有改变,继续执行了,但是这个会引发一些潜在的问题。ABA问题最容易发生在lock free 的算法中的,CAS首当其冲,因为CAS判断的是指针的地址。如果这个地址被重用了呢,问题就很大了。(地址被重用是很经常发生的,一个内存分配后释放了,再分配,很有可能还是原来的地址)

比如上述的DeQueue()函数,因为我们要让head和tail分开,所以我们引入了一个dummy指针给head,当我们做CAS的之前,如果head的那块内存被回收并被重用了,而重用的内存又被EnQueue()进来了,这会有很大的问题。(内存管理中重用内存基本上是一种很常见的行为

这个例子你可能没有看懂,维基百科上给了一个活生生的例子——

你拿着一个装满钱的手提箱在飞机场,此时过来了一个火辣性感的美女,然后她很暖昧地挑逗着你,并趁你不注意的时候,把用一个一模一样的手提箱和你那装满钱的箱子调了个包,然后就离开了,你看到你的手提箱还在那,于是就提着手提箱去赶飞机去了。

这就是ABA的问题。





你可能感兴趣的:(linux内核点滴)