无锁数据结构之——线程安全的SPSC队列

1、Lamport提出的无锁SPSC队列。

在其论文【论文】中证明了,在遵守【顺序一致性】内存模型的计算机中,单生产者单消费者(SPSC)先进先出队列中的锁是可以去除的,从而得到了一个无锁队列,并第一次给出了并发无锁先进先出(Concurrent Lock-free FIFO,CLF)队列的实现。通过去除队列中的锁,生产者、消费者可以并发的访问队列,从而提高了系统的并发执行度。

一种实现:

#define DEFAULT_SIZE 30
template<typename T>
class CircuBuffer
{
public:
	CircuBuffer(unsigned size):
		size_(size)
	{
		writeIndex_=0;
		readIndex_=0;
		buffer_=new T[size_];
	}

	CircuBuffer():
		size_(DEFAULT_SIZE)
	{
		writeIndex_=0;
		readIndex_=0;
		buffer_=new T[size_];
	}

	~CircuBuffer()
	{
		delete [] buffer_;
	}

	unsigned getReadIndex() const
	{
		return readIndex_;
	}

	unsigned getWriteIndex() const
	{
		return writeIndex_;
	}

	bool isEmpty()
	{
		return writeIndex_==readIndex_;
	}

	bool isFull()
	{
		return (readIndex_+1)%size_==writeIndex_;
	}

	bool pushAnEle(T element)
	{
		//队列不为满
		if(!isFull())
		{
			buffer_[writeIndex_]=element;
			writeIndex_=(writeIndex_+1)%size_;
			return true;
		}
		else
		{
			return false;
		}

	};

	T* getAnEle()
	{
		//队列不为空
		if (!isEmpty())
		{
			T* temp=buffer_+readIndex_;
			readIndex_=(readIndex_+1)%size_;
			return temp;
		}
		else
		{
			return nullptr;
		}
	}
private:

	//写指针
	unsigned writeIndex_;
	//读指针
	unsigned readIndex_;
	//环形队列首地址
	T* buffer_;
	//环形队列的尺寸
	unsigned size_;
};

往SPSC型的队列中放入数据的生产者改变【写指针】,消费者改变【读指针】。然而,生产者放入数据至队列中需要判满,这就需要读取【读指针】;同理消费者从队列中取数据,需要判空,这也需要读取【写指针】。若读指针与写指针在同一缓存行,而读写线程分别在不同的核上,这在多核平台上会产生严重的缓存颠簸。所谓的缓存颠簸是运行于多个核上的线程同时修改位于某个缓存行中的不同位置的数据时,导致该缓存行频繁地在多个核上被写无效的现象,这种现象会极大地损害系统的性能。

2、对于Lamport提出的无锁SPSC队列的改进型——FastForward队列。

第一节阐明了Lamport提出的无锁队列存在以下两个缺点:<1>由于生产者与消费者需要使用共享变量头指针与尾指针来同步信息,保存有头指针与尾指针的cacheline会频繁的分别被生产者与消费者修改,产生缓存颠簸,伤害系统的性能;<2>Lamport的CLF队列不能运行在支持弱内存一致性模型的机器中。

【改进1】针对上述去缺点,Join Giacomoni等人提出了针对缓存友好的CLF队列【缓存友好的CLF队列】。

针对缺点2的改进的CLF队列的一种实现:

#ifndef SPSC_QUEUE_H
#define SPSC_QUEUE_H
#include <stdint.h>
#include <string.h>
#define ELE_ZERO 0

class BaseSPSCLockFreeQueue
{

    enum QueueState
    {
        FULL=-1,
        SUCCESS,
        EMPTY
    };

public:
    BaseSPSCLockFreeQueue(int* dataArr,uint32_t maxSize)
    {
        head_=0;
        tail_=0;
        maxQueueSize_=maxSize;
        dataArray_=dataArr;
        memset(dataArray_,ELE_ZERO,maxSize);
    }

    ~BaseSPSCLockFreeQueue()
    {
    }

    //注意 data!=0
    QueueState pushData2Queue(int& dataIn)
    {
        if(dataArray_[head_]!=ELE_ZERO)
            return FULL;

        dataArray_[head_]=dataIn;
        head_=(head_+1)/maxQueueSize_;
        return SUCCESS;
    }

    QueueState getDataFromQueue(int& dataOut)
    {
        if(dataArray_[tail_]==ELE_ZERO)
            return EMPTY;

        dataOut=dataArray_[tail_];
        tail_=(tail_+1)/maxQueueSize_;
        return SUCCESS;
    }

private:
    uint32_t head_;
    uint32_t tail_;
    uint32_t maxQueueSize_;
    int* dataArray_;
};

#endif // SPSC_QUEUE_H

上述针对缺点2的改进队列。在弱内存一致型的机器上也适用。情形一:假定生产者还未将数据放入但已将写指针往后移了一格,且初始时写指针与读指针在同一位置,由于消费者有判据,因此不会出现错误。情形二:假定消费者还未取走新的数据,但读指针已经后移了一格,由于写指针也有判据,因此也不会出现错误。

【改进2】针对存在的缓存行颠簸的问题。主要消除两个方面的缓存行颠簸:1、写指针与读指针错误的缓存行共享;2、数据的错误共享。

针对缺点1的改进的CLF队列的一种实现:

#ifndef MCRINGBUFFER_H
#define MCRINGBUFFER_H

#include <stdint.h>
#include <assert.h>
//x86-64 计算机中为 8*8=64Bytes
#define CacheLineLength 8
#define ELE_ZERO 0
class MCRingBuffer
{

public:
    MCRingBuffer(int* array,size_t maxSize):
        maxSize_(maxSize)
    {
        head_=0;
        tail_=0;
        count_=0;
        buffer_=array;

        for(size_t i=0;i<maxSize;++i)
            buffer_[i]=ELE_ZERO;
    }

    ~MCRingBuffer()
    {
        delete buffer_;
    }

    bool pushData(int& dataIn)
    {
        tempArray[count_++]=dataIn;
        if(count_==CacheLineLength*2)
        {
            for(int i=0;i<CacheLineLength*2;++i)
            {
                if(buffer_[head_]!=ELE_ZERO)
                {
                    count_=0;
                    return false;
                }
                else
                {
                    buffer_[head_]=tempArray[i];
                }
                head_=(head_+1)/maxSize_;
            }
            count_=0;
        }
        return true;
    }

    bool getData(int& dataOut)
    {
        if(buffer_[tail_]==ELE_ZERO)
            return false;

        dataOut=buffer_[tail_];
        buffer_[tail_]=ELE_ZERO;
        tail_=(tail_+1)/maxSize_;
        return true;
    }

private:

    //读指针
    volatile unsigned long tail_;
    long tailPadding_[CacheLineLength-1];
    //写指针
    volatile unsigned long head_;
    long headPadding_[CacheLineLength-1];

    size_t maxSize_;
    int* buffer_;

    unsigned short count_;
    int tempArray[CacheLineLength*2];


};

#endif // MCRINGBUFFER_H

4、实验对比。Lamport与FastForward队列性能对比。


参考:

[1].https://blog.csdn.net/reliveit/article/details/50450136;

[2].

猜你喜欢

转载自blog.csdn.net/weixin_40825228/article/details/80783860