基于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次。耗时如上图。


  • 3
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
很抱歉,作为 AI 语言模型,我无法为您提供代码。但是,我可以为您提供一个基本的思路: 1. 定义一个固定大小的数组作为队列,同时定义一个表示队列头的变量和一个表示队列尾的变量。 2. 定义一个结构体作为队列元素,包含需要存储的数据和一个标记位,表示该元素是否被读取过。 3. 定义一个写线程和多个读线程,写线程用于向队列中添加元素,读线程用于读取队列中的元素。 4. 写线程会首先检查队列是否已满,如果已满则等待,否则将元素添加到队列尾部,并将队列尾指针向后移动一位。 5. 读线程会首先检查队列是否为空,如果为空则等待,否则将队列头的元素读取出来,并将队列头指针向后移动一位。 6. 为了保证线程安全,需要使用原子操作来修改队列头和队列尾的指针。 7. 为了避免多个线程同时读取同一个元素,可以在元素结构体中添加一个标记位,表示该元素是否已被读取过。读线程在读取元素时需要先检查该标记位,如果已被读取过则跳过该元素,否则将该标记位设置为已读取。 8. 为了避免多个线程同时修改同一个元素的标记位,可以使用 CAS(Compare and Swap)操作。 9. 在队列为空时,读线程可以等待一段时间后再次检查队列是否为空,避免过多的空转浪费 CPU 资源。 10. 在队列已满时,写线程可以等待一段时间后再次检查队列是否已满,避免过多的空转浪费 CPU 资源。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值