1、基本原理。源于1994年10月发表在国际并行与分布式会议上的论文【无锁队列的实现.pdf】。CAS(Compare And Swap,CAS维基百科)指令。CAS的实现可参考下面的代码:
bool compare_and_swap (int *accum, int *dest, int newval)
{
if ( *accum == *dest ) {
*dest = newval;
return true;
}
return false;
}
2、实现。
2.1、基于链表的实现。
入队操作:
Enqeuue(x)
{
//准备新加入的结点数据
q=new Data();
q->value=x;
q->next=nullptr;
do{
p=tail;
}while(!CAS(p->next,nullptr,q));
}
CAS(tail,p,q);
上述实现有个潜在的问题,若某个线程在将尾结点更新至新加入的结点之前,即语句CAS(tail,p,q)之前挂掉了,那么其他的所有线程在进行入队时将会在do...while代码段无限循环,因为CAS一直返回为false。因为p->next不可能为nullptr(见下图)。
入队操作改进:
Enqueue(x)
{
q=new Data();
q->value=x;
q->next=nullptr;
p=tail;
oldP=p;
do
{
while(p->next!=nullptr)
p=p->next;
}while(!CAS(p->next,nullptr,q));
CAS(tail,oldP,q);
}
这样即使其中有某个线程在更新tail之前挂掉了,在进入do...while循环后,p将会被置为指向队列最后一个元素。从而CAS为true,结束while循环(参考下面的图示)。
出队操作;
DeQueue()
{
do
{
p=head;
if(p->next==nullptr)
return EMPTY;
}while(CAS(head,p,p->next);
return p->next->value;
}
注意:
为了避免在队列中只有一个元素时,队头与队尾指针指向同一个元素,在初始化队列时,队头与队尾均指向同一个哑元结点。
上述实现无法避免ABA问题。
上面的算法出现的ABA问题:
假定某个线程准备出队操作,首先声明一个指向p指针head结点,接着要进行CAS操作,CAS(head,p,p->next)。假定在执行CAS操作之前,有个线程进行了入队操作,此时,head!=p,正常情形CAS(head,p,p->next)应该返回为false。但是,在CAS(head,p,p->next)之前,又有线程进行了入队操作,而入队的这个结点占用的内存恰恰是最开始的时候p所指向的内存,再恰恰经过一系列出队操作,使得当前头指针刚好指向刚刚入队操作的那块结点,最后,才开始,进行CAS操作。我们会发现原本应该返回为false的CAS操作,返回了true!(CAS比较的是地址,==)。
那么问题来了,如何避免ABA问题?//To do
2.2、基于数组实现的环形无锁队列。
使用数组来实现队列是很常见的方法,因为没有内存的申请与释放,一切都会变得简单。实现思路:
<1>、队列实现的形式是环形数组的形式;
<2>、队列的元素的值,初始的时候是三种可能的值。HEAD、TAIL、EMPTY;
<3>、数组一开始所有的元素都初始化为EMPTY。有两个相邻的元素初始化为HEAD与TAIL,代表着空队列;
<4>、入队操作。假设数据x要入队列,定位TAIL的位置,使用double-CAS方法把(TAIL, EMPTY) 更新成 (x, TAIL)。需要注意,如果找不到(TAIL, EMPTY),则说明队列满了。
<5>、出队操作。定位HEAD的位置,把(HEAD, x)更新成(EMPTY, HEAD),并把x返回。同样需要注意,如果x是TAIL,则说明队列为空。
一种实现:
MPMCQUEUE.h
#ifndef MPMCQUEUE_H
#define MPMCQUEUE_H
enum ELE_VALUE
{
HEAD=-2,
TAIL,
EMPTY
};
enum QUEUE_STATE
{
QUE_NOMAL=-5,
QUE_EMPTY,
QUE_FULL
};
#include
#include
class MPMCQueue
{
public:
MPMCQueue(int* array,size_t maxSize)
{
buffer_=array;
maxSize_=maxSize;
for(size_t i=0;i
MPMCQUEUE.cpp
#include "MPMCQueue.h"
int MPMCQueue::tailFlag_=TAIL;
int MPMCQueue::headFlag_=HEAD;
int MPMCQueue::emptyFlag_=EMPTY;
3、试验验证。
4个线程插入,4个线程取出。每个线程插入或者删除100w次。耗时如上图。