【问题标题】:Lock-free stack pop implementation in C++C ++中的无锁堆栈弹出实现
【发布时间】:2020-09-14 23:18:43
【问题描述】:

在 C++ Concurrency in Action, 2e 中,作者描述了一种无锁线程安全链表实现。现在,他正在描述 pop() 方法以及如何以一种类似“垃圾收集器”的方法安全地删除节点,以确保没有其他线程在同一实例上调用 pop。这是pop的一些代码:

#include <atomic>
#include <memory>

template<typename T>
class lock_free_stack
{
private:
    std::atomic<unsigned> threads_in_pop;
    void try_reclaim(node* old_head);
public:
    std::shared_ptr<T> pop()
    {
        ++threads_in_pop;
        node* old_head=head.load();
        while(old_head &&
              !head.compare_exchange_weak(old_head,old_head->next));
        std::shared_ptr<T> res;
        if(old_head)
        {
            res.swap(old_head->data);
        }
        try_reclaim(old_head);
        return res;
    }
};

重要的是每次调用 pop() 时计数器都会自动递增。然后,try_reclaim 函数将递减所述计数器。下面是 try_reclaim 的实现:

void try_reclaim(node* old_head)
{
    if(threads_in_pop==1) //#1
    {
        node* nodes_to_delete=to_be_deleted.exchange(nullptr);
        if(!--threads_in_pop) //#2
        {
            delete_nodes(nodes_to_delete);
        }
        else if(nodes_to_delete)
        {
            chain_pending_nodes(nodes_to_delete);//#3
        }
        delete old_head; //#4 THIS IS THE PART I AM CONFUSED ABOUT
    }
    else
    {
        chain_pending_node(old_head);
        --threads_in_pop;
    }
}

这里调用的其他函数的实现是无关紧要的(它们只是将节点添加到要删除的节点链中),所以我省略了它们。我在代码中感到困惑的部分是#4(标记)。这里,作者对传入的old_head调用了delete。但是,为什么不在删除old_head之前检查一下threads_in_pop此时是否仍然为零呢?他在第 2 行和第 1 行再次检查以确保另一个线程当前不在 pop() 中,那么他为什么不在继续删除 old_head 之前再次检查呢?难道另一个线程不可能在#3 之后立即调用pop(),从而增加计数器,当第一个线程到达#4 时,threads_in_pop 将不再为零?

也就是说,在代码到达#4时,threads_in_pop不可能是例如2吗?在那种情况下,他怎么能安全地删除 old_head 呢?有人可以解释一下吗?

【问题讨论】:

    标签: c++ multithreading data-structures concurrency lock-free


    【解决方案1】:

    您有以下(简化的)序列,并且所有原子操作都是顺序一致的:
    ++threads_in_pop -&gt; head.cmpxchg -&gt; threads_in_pop.load() -&gt; delete old_head

    所以我们先把当前的head去掉,然后检查threads_in_pop的数量。假设我们有两个线程,T1 和 T2,它们在堆栈上运行。如果 T1 在 try_reclaim 中执行 threads_in_pop.load() (#1) 并看到 1,这意味着 T2 尚未执行递增 (++threads_in_pop),即 T1 是唯一可以引用 old_head 的线程在那时候。但是,T1 已经从列表中删除了old_head,因此任何进入 pop 的线程都将看到更新的头部,因此其他线程不可能再获得对old_thread 的引用。因此删除old_head 是安全的。

    检查#2 是必要的,以避免释放刚刚添加到to_be_released 列表的节点此线程已执行其弹出,但其他线程可能仍持有引用。考虑以下情况:

    • T1 执行弹出,即将执行nodes_to_delete=to_be_deleted.exchange(nullptr);
    • T2 开始弹出并读取head
    • T3 启动 pop 并读取 head,看到与 T2 相同的值
    • T2 完成弹出并将old_head 添加到列表中
    • 注意:T3 仍然具有对现在是 to_be_deleted 列表一部分的节点的引用
    • T1 现在执行nodes_to_delete=to_be_deleted.exchange(nullptr);

    T1 现在有一个 nodes_to_delete 列表,其中包含对 T3 仍可访问的节点的引用。这就是为什么需要检查 #2 以防止 T1 释放该节点。

    【讨论】:

      【解决方案2】:

      调用try_reclaim 的线程刚刚从堆栈中删除了old_head

      该类确保任何 old_head 的其他使用必须在来自其他线程的pop 调用中,因此如果线程发现没有其他并发调用,那么它知道它是old_head 指针的唯一持有者。然后,只要它不发布该指针以便它可能从另一个线程中获取,它就可以在它接近它时将其删除。

      所以实施是安全的。你问的问题:“他为什么不[再次]检查”表明你的想法不正确。再次检查并不能证明任何事情,因为如果另一个线程有可能进入pop 并使用old_head,那么它可能总是在你检查之后发生

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-12-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-11-27
        • 1970-01-01
        • 2023-03-31
        相关资源
        最近更新 更多