基于CAS实现的无锁队列(多生产者多消费者)

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 <stdint.h>
#include <atomic>
class MPMCQueue
{
public:
    MPMCQueue(int* array,size_t maxSize)
    {
        buffer_=array;
        maxSize_=maxSize;
        for(size_t i=0;i<maxSize_;++i)
            buffer_[i]=EMPTY;

        buffer_[0]=TAIL;
        buffer_[1]=HEAD;

        head_.store(1);
        tail_.store(0);

    }

    ~MPMCQueue()
    {

    }

    QUEUE_STATE pushData(const int& dataIn)
    {
        std::atomic_int temp;
        do{
            unsigned short headNext=(head_.load()+1)&(maxSize_-1);

            if(buffer_[headNext]!=EMPTY)
                return QUE_FULL;

            temp.store(buffer_[headNext]);
        }while(!temp.compare_exchange_weak(emptyFlag_,headFlag_));

            buffer_[head_.load()]=dataIn;

            head_.fetch_add(1);
            return QUE_NOMAL;
    }

    QUEUE_STATE getData(int& dataOut)
    {
        std::atomic_int temp;
        unsigned short tailNext;
        do
        {
            tailNext=(tail_.load()+1)&(maxSize_-1);
            if(buffer_[tailNext]==HEAD)
                return QUE_EMPTY;

            temp.store(buffer_[tail_.load()]);

        }while(!temp.compare_exchange_weak(tailFlag_,emptyFlag_));

        dataOut=buffer_[tailNext];
        buffer_[tailNext]=TAIL;

        tail_.fetch_add(1);
        return QUE_NOMAL;
    }

private:

    static int tailFlag_;

    static int headFlag_;

    static int emptyFlag_;

    std::atomic_ushort head_;
    std::atomic_ushort tail_;
    size_t maxSize_;
    int* buffer_;
};

#endif // MPMCQUEUE_H

MPMCQUEUE.cpp

#include "MPMCQueue.h"
int MPMCQueue::tailFlag_=TAIL;
int MPMCQueue::headFlag_=HEAD;
int MPMCQueue::emptyFlag_=EMPTY;

3、试验验证。


4个线程插入,4个线程取出。每个线程插入或者删除100w次。耗时如上图。


猜你喜欢

转载自blog.csdn.net/weixin_40825228/article/details/80801596
今日推荐