为啥“删除”这个无锁堆栈类中的节点会导致竞争条件?

Posted

技术标签:

【中文标题】为啥“删除”这个无锁堆栈类中的节点会导致竞争条件?【英文标题】:Why would 'deleting' nodes in this lock-free stack class would cause race condition?为什么“删除”这个无锁堆栈类中的节点会导致竞争条件? 【发布时间】:2016-09-16 18:37:51 【问题描述】:

在 Anthony Williams 的《C++ Concurrency in Action》一书中,第 7.2.1 节列出了无锁堆栈实现:

template <typename T>
class lock_free_stack 
    struct node 
        shared_ptr<T> data_;
        node* next_;
        node(const T& data) : data_(make_shared(data)) 
    ;
    atomic<node*> head_;
public:
    void push(const T& data)
    
        node* new_node = new node(data);
        new_node->next_ = head_.load();
        while(!head.compare_exchange_weak(new_node->next_, new_node));
    
    shared_ptr<T> pop()
    
        node* old_head = head_.load();
        while (old_head &&
                !head_.compare_exchange_weak(old_head, head_->next_));
        return old_head ? old_head->data_ : shared_ptr<T>();
    
;

然后在第 7.2.2 节中,作者说“......在 pop() 中,我们选择泄漏节点以避免竞争条件,即一个线程删除一个节点而另一个线程仍然持有指向它的指针它即将取消引用。”

1) 我不明白为什么会发生这种情况以及为什么下面的 pop() 函数会导致竞态条件:

shared_ptr<T> pop()

    node* old_head = head_.load(); // (1)
    while (old_head &&
            !head_.compare_exchange_weak(old_head, head_->next_)); // (2)
    shared_ptr<T> res; // (3)
    if (old_head) 
        res.swap(old_head->data);
        delete old_head;
        return res;
     else 
        return ;
    

2) 为什么多个线程同时调用pop(),'old_head'变量在第(3)行之后可以指向同一个节点对象?

【问题讨论】:

【参考方案1】:

线程 1 进行到 (2)。它开始评估head_-&gt;next。它将head_ 加载到寄存器中,然后放弃优先级。

线程 2 从函数开始到结束。它通过删除 head_ 将其从存在中移除并返回 head_ 的内容。

线程 1 唤醒。它遵循head_ 在寄存器中获取-&gt;next 字段。但是线程2已经删除了head_指向的数据,我们只是跟着一个悬空指针。

【讨论】:

对不起,我还不清楚。是的,线程 1 将跟随不存在的指针并在“下一个”中读取值,该值可能已经分配给另一个变量。因此, compare_exchange_weak() 的第二个参数将无效。但它永远不会被使用,因为当 compare_exchange_weak() 将被执行时,它会检测到“head_!= old_head”。所以,是的,我们将读取 compare_exchange_weak() 的无效参数,但它永远不会被使用。问题是什么?你能澄清一下吗? @alex 为什么你认为它们不相等?你跟着一个悬空指针,值可以是anything。它在哪里检查head_!=old_head_?我错过了吗? 很可能我错过了一些东西,但我仍然无法理解到底是什么。检查“head_!=old_head_”是否在 compare_exchange_weak() 中。如果这些值不相等,则不会使用 compare_exchange_weak() 的第二个参数。在我的理解中 compare_exchange_weak() 以原子方式从内存中读取 head 的当前值。在第一个线程唤醒后讨论的场景中,寄存器中的头值将不等于内存中头的当前值。因此, compare_exchange_weak() 会看到它并且不会使用第二个参数(垃圾,我们在寄存器中读取以下无效值的 head)。 @alex 你的意思是old_head 不是old_head_【参考方案2】:

我有同样的困惑,并试图用谷歌搜索答案......我找不到答案,最后去检查 compare_exchange_weak 参考。 我们缺少的部分是您传入第二个所需参数的时间,您已经取消了对悬空指针的引用...... 您无法真正摆脱它,因为该函数需要知道您传入的内容,从而取消引用它。

【讨论】:

以上是关于为啥“删除”这个无锁堆栈类中的节点会导致竞争条件?的主要内容,如果未能解决你的问题,请参考以下文章

为啥类中的 stringstream 成员会导致编译时错误? [复制]

以下无锁代码是不是表现出竞争条件?

MPSC 队列:竞争条件

为啥这个 BigInteger 值会导致堆栈溢出异常? C#

为啥我的代码会导致 java 中的堆栈溢出错误?它最终应该终止。 for循环版本不会导致错误

为啥增加递归深度会导致堆栈溢出错误?