簡體   English   中英

簡單的無鎖棧 C++11

[英]Simple lock free stack c++11

我已經在 C++ 中看到了幾個過於復雜的(在我看來顯然)無鎖堆棧的實現(使用像這里的標簽),我想出了我認為是一個簡單但仍然有效的實現。 由於我在任何地方都找不到這個實現(我已經看到 Push 函數的實現與我所做的類似,但不是 Pop),我猜測它在某些方面是不正確的(最有可能在 ABA 案例中失敗):

template<typename Data>
struct Element
{
  Data mData;
  Element<Data>* mNext;
};

template<typename Data>
class Stack
{
 public:
  using Obj = Element<Data>;
  std::atomic<Obj*> mHead;

  void Push(Obj *newObj)
  {
    newObj->mNext = mHead.load();
    //Should I be using std::memory_order_acq_rel below??
    while(!mHead.compare_exchange_weak(newObj->mNext, newObj));
  }

  Obj* Pop()
  {
    Obj* old_head = mHead.load();
    while (1)
    {
      if (old_head == nullptr)
        return nullptr;
      //Should I be using std::memory_order_acq_rel below??
      if(mHead.compare_exchange_weak(old_head, old_head->mNext)) ///<<< CL1
        return old_head;
    }
  }
};

我假設 Push 和 Pop 的調用者將負責內存分配和釋放。 另一種選擇是將上述 Push 和 Pop 方法設為私有方法,並擁有新的公共函數來處理內存分配並在內部調用這些函數。 我相信這個實現中最棘手的部分是我用“CL1”標記的那一行。 我認為它是正確的並且在 ABA 案例中仍然有效的原因如下:

可以說ABA案例確實發生了。 這意味着“CL1”處的 mHead 將等於 old_head,但它們指向的對象實際上與我將 mHead 分配給 old_head 時最初指向的對象不同。 但是,我認為即使它是一個不同的對象,我們仍然可以,因為我們知道它是一個有效的“頭”。 old_head 指向與 mHead 相同的對象,因此它是堆棧的有效頭,這意味着 old_head->mNext 是有效的下一個頭。 所以,將 mHead 更新為 old_head->mNext 仍然是正確的!

總結一下:

  1. 如果 mHead != old_head(另一個線程搶占我們並更改了 mHead)-> old_head 被更新為新的 mHead,我們再次開始循環。
  2. [NON-ABA] 如果 mHead == old_head -> 簡單情況,將 mHead 更新為 old_head->next (==mHead->mNext) 並返回 old_head。
  3. [ABA] 如果 mHead == old_head -> 如上所述。

那么,我的實現有效嗎? 我錯過了什么?

ABA 發生在:

  1. 線程 A 在調用compare_exchange_weak之前讀取old_head->mNext並阻塞。
  2. 線程 B 彈出當前節點並推入一些其他節點,然后將原始節點推回堆棧。
  3. 線程 A 解除mHead ,成功完成compare_exchange_weak因為mHead具有相同的值,但將陳舊的mNext值存儲為新的mHead

有關更多詳細信息請參閱此答案,您有問題 #2( mNext上的數據競爭)和問題 #3(ABA)。

一般來說,如果數據類型是按位正則的,那么實現可能是正確的,如果只是半正則(或相等不是按位),則不可避免地會受到 ABA 的影響。

如果數據類型是按位規則的,則 CAS 操作就足夠了,例如對於整數。 ABA 不是問題,因為 A 等於 A,故事結束。

Harris 算法似乎允許非按位正則類型提供集合實現,但以無界性能為代價,更糟糕的是,它回避了如何分配列表節點的問題。 這意味着要解決的核心問題是提供一個 O(1) 無鎖分配器,而這樣的數據結構是不存在的,因為所涉及的指針只是半規則的。 特別是,當指針僅將字節數組標識為內存塊時,它是規則的,當它標識鏈表的節點時,它不再是規則的,因為指針相等並不能確保嵌入的下一個指針相等。

沒有可能的解決方法。 我懷疑 DCAS 會有所幫助,我也懷疑將恆定時間限制放寬到線性是否有效。

對於不可搶占的線程,只要它保護的臨界區是有界時間,自旋鎖就可以工作。 您可以在 MacOS 和 RT-Linux 上獲得這些。

如果你需要跨平台,這個lfstack可以做跨平台構建,它是c native內置的

例子:-

int* int_data;
lfstack_t mystack;

if (lfstack_init(&mystack) == -1)
    return -1;

/** Wrap This scope in other threads **/
int_data = (int*) malloc(sizeof(int));
assert(int_data != NULL);
*int_data = i++;
/*PUSH*/
 while (lfstack_push(&mystack, int_data) == -1) {
    printf("ENQ Full ?\n");
}

/** Wrap This scope in other threads **/
/*POP*/
while  ( (int_data = lfstack_pop(&mystack)) == NULL) {
    printf("POP EMPTY ..\n");
}

// printf("%d\n", *(int*) int_data );
free(int_data);
/** End **/

lfstack_destroy(&mystack);

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM