如何在此无锁堆栈函数中防止未定义的行为和ABA问题?

时间:2015-11-03 00:43:24

标签: c++ multithreading c++11 compare-and-swap

我目前正在使用C ++ 11中的无锁单链表,我的popFront()函数存在问题 - 或者我至少应该说我知道它会有在某些情况下的问题。

无论如何,这就是我目前所拥有的:

std::shared_ptr<T> popFront(void)
{
    auto p = atomic_load(&head);
    while(p && !atomic_compare_exchange_weak(&head, &p, p->next))
    {}
    return p ? p->data : std::shared_ptr<T>();
}

请注意,head的类型为shared_ptr

但是,我预计会遇到一些问题。第一种情况是两个线程正在执行popFront(),它们都读取相同的head,并且一个线程首先完成。在第二个线程完成之前,调用者删除指向的对象,因此第二个线程正在使用已删除的内存。第二个问题是经典的ABA问题。

这个链表背后的想法是让它无锁,所以我想避免在这个函数中强加一个锁。不幸的是,我不知道如何解决这些问题。任何建议将不胜感激。

2 个答案:

答案 0 :(得分:0)

在没有ABA问题的情况下,设计无锁队列有很多解决方案。

article应提供一些见解,并且可以找到解决此问题的一些常用工具here

现在,根据您提到的所述问题:

  

在第二个线程完成之前,调用者删除指向的对象,因此第二个线程正在使用   删除内存

是的,可能会发生并且解决方案是使用tagged pointers:在32位架构上,不使用最后2个(or more)位,因此它们可用于标记和64在比特架构中,我们至少有3个未使用的比特。

因此我们可以设置为逻辑删除指针,但不能通过设置指针的一些未使用位来物理删除它,如下所示:

__inline struct node* setTag(struct node* p, unsigned long TAG)
{
    return (struct node*) ((uintptr_t)p | TAG);
}

__inline bool isTagged(struct node* p, unsigned long TAG)
{
    return (uintptr_t)p == (uintptr_t)p &  ~TAG;
}

__inline struct node* getUntaggedAddress(struct node* p, unsigned long TAG)
{
    return (struct node*)((uintptr_t)p &  ~TAG);
}

其中TAG最多为4(对于32位架构),在64位架构上最多为8(根据计算机架构和字对齐,有2/3或更多未使用的位)。

现在做CAS时,我们忽略了标记指针=&gt;因此只对有效指针进行操作。

在队列上出列队列时,我们可以按如下方式执行:

int dequeue(qroot* root)
{
    qnode* oldHead;

    do
    {
        oldHead = root->head;

        if (isTagged(root->head)) //disregard tagged addresses
            return NULL;

        oldHead = getUntaggedAddress(root->head); //we do a CAS only if the old head was unchanged

    } while (root->head.compare_exchange_strong(oldHead, oldHead->next, std::memory_order_seq_cst));

    return &(oldHead->data);
}

给出

typedef struct qnode
{
    std::atomic<qnode*> next;
    int data;
}qnode;

typedef struct qroot
{
    std::atomic<qnode*> head; //Dequeue and peek will be performed from head
    std::atomic<qnode*> tail; //Enqueue will be performed to tail
}qroot;

答案 1 :(得分:0)

有助于使线程更容易的一件事就是不释放内存。如果您正在使用一堆链表节点,则可以考虑使用它们的池。您可以将其返回池中,而不是释放节点。这可以处理部分问题。

ABA很简单。每次更改头指针时都需要碰撞计数器。你需要同时用指针原子地写这个计数器。如果使用32位寻址,则使用64位比较和交换(CAS),并将计数器存储在额外的32位中。如果使用64位寻址,请避免128位比较和交换,因为它可能很慢(在我们的Xenon芯片上,任何高达64位的速度都很快)。由于Windows和Linux都不支持完整的64位寻址,因此您可以使用64位中的一些用于ABA。我使用联合为32位和64位寻址模式执行此操作。

您无需计算每项更改。你只需抓住每一个变化。即使试图让ABA有很多线程尽可能快地改变头部,它仍然很少发生。在现实生活中,它很少发生。即你不需要非常高。我通常使用4位并让它滚动。我可能会遇到麻烦。如果需要,可以使用更多。

在这个例子中,我假设64位,并使用CAS()进行比较和交换,因此您必须替换编译器用于CAS的任何内容:

typedef unsigned __int64_t U8;

struct TNode {
    TNode* m_pNext;
};

template<class T>
union THead {
    struct {
        U8  m_nABA : 4,  
            m_pNode:60;  // Windows only supports 44 bits addressing anyway.
    };
    U8  m_n64; // for CAS
    // this constructor will make an atomic copy on intel 
    THead(THead& r)         { m_n64 = r.m_n64; }
    T* Node()               { return (T*)m_pNode; }
    // changeing Node bumps aba
    void Node(T* p)         { m_nABA++; m_pNode = (U8)p; return this; }
};

// pop pNode from head of list.
template<class T>
T* Pop(volatile THead<T>& Head) {
    while (1) { // race loop
        // Get an atomic copy of head and call it old.
        THead<T> Old(Head);
        if (!Old.Node())
            return NULL;
        // Copy old and call it new.
        THead<T> New(Old);
        // change New's Node, which bumps internal aba
        New.Node(Old.Node()->m_pNext);
        // compare and swap New with Head if it still matches Old.
        if (CAS(&Head.m_n64, Old.m_n64, New.m_n64))
            return Old.Node(); // success
        // race, try again
    }
}

// push pNode onto head of list.
template<class T>
void Push(volatile THead<T>& Head, T* pNode) {
    while (1) { // race loop
        // Get an atomic copy of head and call it old.
        // Copy old and call it new.
        THead<T> Old(Head), New(Old);
        // Wire node t Head
        pNode->m_pNext = New.Node();
        // change New's head ptr, which bumps internal aba
        New.Node(pNode);
        // compare and swap New with Head if it still matches Old.
        if (CAS(&Head.m_n64, Old.m_n64, New.m_n64))
            break; // success
        // race, try again
    }
}