利用std::atomic实现无锁队列

队列基础,一对一无锁

队列就是一个先进先出的数据结构,一般使用链表实现,如下图:

数据从Head进入链表,从Tail出链表。我们会发现,只要链表一直不是空的,那么Head指针和Tail指针是完全无关的,也就是读写之间不加锁的线程安全比较容易实现,我们只要保证链表从初始化开始Head和Tail就无关即可,那么一对一的生产者和消费者无锁队列就可以十分简单地实现,显然Tail或者Head直接指向的内存我们不用来存储数据,是最合理的。

上述两种方式都可以,我们采用第一种展开讲述。在队列创建时,我们让Head和Tail共同指向一个不存储数据的内存,从此这两个指针就可以毫不相干了,后续生产消费中,Tail总是预留一个不存储数据的内存节点,而Head则只需要完成自己的指向操作。以下是相关代码:

template <typename T>
class T1V1Queue : NonCopyable {
 public:
  using ElementType = T;
  T1V1Queue() : mSize(0) { mHead = mTail = new TNode; }
  ~T1V1Queue() {
    while (mTail != nullptr) {
      TNode* pNode = mTail;
      mTail = pNode->NextNode;
      delete pNode;
    }
  }
  bool Dequeue(ElementType& OutItem) {
    TNode* pPop = mTail->NextNode;
    if (pPop == nullptr) return false;
    OutItem = std::move(pPop->Item);
    TNode* pOldTail = mTail;
    mTail = pPop;
    mTail->Item = ElementType();
    delete pOldTail;
    mSize--;
    return true;
  }

  bool Enqueue(const ElementType& InItem) {
    TNode* pNewNode = new (std::nothrow) TNode(InItem);
    if (pNewNode == nullptr) return false;
    TNode* OldHead = mHead;
    mHead = pNewNode;
    std::atomic_thread_fence(std::memory_order_seq_cst);
    OldHead->NextNode = pNewNode;
    mSize++;
    return true;
  }

  FORCEINLINE bool IsEmpty() const { return mSize == 0; }

  ElementType* Peek() {
    if (mTail->NextNode == nullptr) return nullptr;
    return &(mTail->NextNode->Item);
  }

  FORCEINLINE const ElementType* Peek() const { return const_cast<T1V1Queue*>(this)->Peek(); }

  bool Pop() {
    TNode* pPop = mTail->NextNode;
    if (pPop == nullptr) return false;
    TNode* pOldTail = mTail;
    mTail = pPop;
    mTail->Item = ElementType();
    delete pOldTail;
    mSize--;
    return true;
  }

  void Empty() {
    while (Pop())
      ;
  }

  uint64_t GetSize() const { return mSize; }

 private:
  struct TNode {
    TNode* volatile NextNode;
    ElementType Item;
    TNode() : NextNode(nullptr) {}
    explicit TNode(const ElementType& InItem) : NextNode(nullptr), Item(InItem) {}
    explicit TNode(ElementType& InItem) : NextNode(nullptr), Item(std::move(InItem)) {}
  };

  TNode* volatile mHead;
  TNode* mTail;

  std::atomic_uint64_t mSize;
};

生产者之间无锁

为了实现多生产者之间的无锁操作,我们需要对上述的代码进行一定的修改,引入原子变量std::atomic,需要把Head指针改为:  std::atomic<TNode*> mHead。

指针的修改的原子操作,我们需要使用std::atomic_compare_exchange_weak(或者std::atomic_compare_exchange_strong),下面的代码可以保证多线程的情况下,链表可以保证正确性:

bool Enqueue(const ElementType& InItem) {
  TNode* pNewNode = new (std::nothrow) TNode(InItem);
  if (pNewNode == nullptr) return false;

  TNode* pCurrHead = mHead;
  while (!std::atomic_compare_exchange_weak(&mHead, &pCurrHead, pNewNode))
    ;
  std::atomic_thread_fence(std::memory_order_seq_cst);
  pCurrHead->NextNode = pNewNode;
  mSize++;
  return true;
}

std::atomic_compare_exchange_weak进行比较和赋值的原子操作,当前线程先获取了一个Head的当前值,然后原子地比较和修改Head的指向,成功则修改Head指向,失败则pCurrHead被赋值为新的mHead,然后再重复上述原子操作,直至成功。std::atomic_thread_fence(std::memory_order_seq_cst);是一个内存屏障,可以保证屏障前后的操作不被重排。

消费者之间的无锁

完全无锁队列就差消费者之间的无锁了,这一步我们很自然想到,可以对Tail指针使用与Head相同的处理,但是这里我们会遇到一个难题,因消费过程我们需要释放内存,否则就会内存泄漏,从上面可以看到,我们实际上并不是直接消费Tail->Item,而是消费Tail->NextNode->Item,在多线程消费的情况下,Tail->NexNode极有可能被其他线程销毁,导致程序崩溃。

那么采用Head指向无数据节点会不会好点?不管哪种方式,我们移动Tail指针都是如下的代码:

TNode* pTail = mTail;
do 
{
  if (pTail->NextNode == nullptr) return false;
} while (!std::atomic_compare_exchange_weak(&mTail, &pTail, pTail->NextNode));

pTail指向的内存随时被其它线程delete。因此在消费的过程中我们不能随便delete尾部的内存,否则整个消费过程就会程序崩溃。我们需要进行一个延迟删除的操作,从而保证队列的指针在消费者消费的时候是正常的。

延迟删除我们需要先标记链表上被删除的元素,然后再找一个适当的时机销毁这些被标记的元素,目前可以轻易想到的是时间上的延迟和预留足够的数量两种方式,其中时间上的延迟需要对被标记的元素记录一个时间戳,真正执行删除操作时与当前时间进行比较,基于时间的延迟删除,在队列吞吐量特别大时,会占用较多的内存;基于预留特定数量的删除在内存使用上会好一些,但风险上会比时间延迟要大一些。

删除操作我们可以非常容易原子性地交给某个消费者线程,只需要对一个原子变量执行++操作判断是否是1

uint64_t uCurrSize = --mSize;
uint64_t uNeedDelNum = mListSize.load() - uCurrSize;
if (uNeedDelNum > 256) {
  if (++mUniqDelFlag == 1) {
    while (mDelTail->NextNode != nullptr && mDelTail->NextNode->BeDeleted && uNeedDelNum > 256) {
      TNode* pDel = mDelTail;
      mDelTail = mDelTail->NextNode;
      mListSize--;
      uNeedDelNum--;
      delete pDel;
    }
  }
  --mUniqDelFlag;
}

我们始终保护性地保留了256个元素,并且我们是按照顺序delete,遇到没被标记删除的我们就停止。

详细代码见:GitHub - laiyongcong/cppfoundation: c++ basic library

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值