两个线程从无锁堆栈中删除

时间:2014-03-16 05:10:17

标签: c++ multithreading concurrency stack lock-free

我指的是C ++ Concurrency in Action中的第185和186页。他们将以下代码作为无锁堆栈的方法:

void push(T const& data){
    node* const new_node = new node(data);
    new_node->next=head.load();
    while(!head.compare_exchange_weak(new_node->next, new_node));
}

并在P186上说明如下:

  

当然,现在您已经有了向堆栈添加数据的方法了   需要一种让它再次脱落的方法。从表面上看,这是相当的   简单:

     
      
  1. 阅读head
  2. 的当前值   
  3. 阅读head->next
  4.   
  5. head设为head->next
  6.   
  7. 从检索到的节点返回数据
  8.   
  9. 删除检索到的节点
  10.         

    但是,在存在多个线程的情况下,这并不是那么简单。如果   有两个线程可以从堆栈中删除它们   在步骤1中读取head的相同值。如果一个线程继续   在另一个到达第2步之前,一直到第5步   第二个线程将取消引用悬空指针。

我认为compare_exchange_weak()可以用来基本完成第2步和第3步,第二个线程可以看到head->next不再有效?

我很惊讶我们不能用CAS来解决上述问题??

1 个答案:

答案 0 :(得分:0)

当关闭项目时,您不会更改项目 - >接下来,您将改变头部指向项目 - >接下来。 在您将项目从列表中删除之前,不允许更改项目。如果另一个线程更改了item-> next,则意味着他们已将其从列表中弹出,这意味着ABA版本将不同(我假设您在这样的链接列表中使用ABA检查)并且可能(这就是为什么你使用ABA - 因为这里的“可能”)头部会有所不同,所以你的CAS会失败。

关于ABA的注意事项,如果你的项目一旦弹出就永远不会把它放回到列表中,并且你永远不会释放任何项目(这将允许项目的内存被重用并被卡在列表中),那么你就不会需要ABA检查。但是,它只需要几个位来实现ABA,因为您只需要在从列表中弹出项目所需的时间内可能发生的版本。我喜欢至少16岁,但这可能会让它接近尾声。每次添加项目都会更改ABA版本。每次删除项目时,请更改ABA版本。

其他重要说明。像这样的原子列表中的项目并不意味着使用以原子方式操作的head / next遍历。这是因为您永远不知道是否有人关闭该项目并接下来更改。我发现,如果您在列表中的其他内容旁边更改,例如跳过某个项目,则可以在某些情况下使用。像这样的原子列表中的项永远不会被添加或删除除列表头之外的任何地方。你无法通过原子操作来完成这项工作(很容易......永远不会说永远不会)。

这是一个适用于msvc 2010的示例。这使用了Microsoft的volatile的版本。如果你的编译器是iso,你可以使用std :: atomic。注意使用union。 union CAtomicLinkList是我们一次原子地设置多个字段的方式。

template <typename T> class CAtomicListItem : public T {
  typedef CAtomicListItem<T> CItem;
public:
  CItem*      m_pNext;

  CItem()  : m_pNext(NULL)                               { }
  CItem(T &r) : T(r),  m_pNext(NULL)                     { }
};

template <typename T> union CAtomicLinkList {
  typedef CAtomicLinkList<T> CList;
  typedef CAtomicListItem<T> CItem;
public:
  struct {
    QWORD     m_pHead : 44,    // Addr of first item.  Windows only uses 8tb
                      :  4,    // extra space.
              m_nABA  : 16;    // Version to prevent ABA.  We don't need this 
                               // many bits so there is room for expansion.
  };
  QWORD        m_n64;          // atomically update this struct by CAS on this field.

  CList() : m_n64(0)                                     { }
  // These constructors are for making copies.  These cannot threadsafe 
  // update a shared instance.
  CList(volatile const CList& r) : m_n64(r.m_n64)        { }

  void Push(CItem *pItem) volatile {
    while (1) {
      CList Old(*this), New(Old);
      New.m_pHead = UINT_PTR(pItem);
      New.m_nABA++;  // whenever you change the list, change the version
      pItem->m_pNext = (CItem*)Old.m_pHead;
      if (CAS(&m_n64, Old.m_n64, New.m_n64))
        return; // success
    }
  }

  CItem* Pop() volatile {
    while (1) {
      CList Old(*this);
      if (!Old.m_pHead)
        return NULL;
      CList New(Old);
      CItem* pItem = (CItem*)Old.m_pHead;
      New.m_pHead = UINT_PTR(pItem->m_pNext);
      New.m_nABA++; // whenever you change the list, change the version
      if (CAS(&m_n64, Old.m_n64, New.m_n64))
        return pItem; // success
    }
  }
};

inline bool CAS(volatile WORD* p, const WORD nOld, const WORD nNew) {
  Assert(IsAlign16(p)); 
  return WORD(_InterlockedCompareExchange16((short*)p, nNew, nOld)) == nOld; 
}
inline bool CAS(volatile DWORD* p, const DWORD nOld, const DWORD nNew) {
  Assert(IsAlign32(p)); 
  return DWORD(InterlockedCompareExchange((long*)p, nNew, nOld)) == nOld; 
}
inline bool CAS(volatile QWORD* p, const QWORD nOld, const QWORD nNew) { 
  Assert(IsAlign64(p)); 
  return QWORD(InterlockedCompareExchange64((LONGLONG*)p, nNew, nOld)) == nOld; 
}
inline bool CAS(volatile PVOID* pp, const void *pOld, const void *pNew) {
  Assert(IsAlign64(pp)); 
  return PVOID(InterlockedCompareExchangePointer(pp, (LPVOID)pNew, (LPVOID)pOld)) == pOld; 
}