当前位置: 首页 > 知识库问答 >
问题:

这个使用锁的线程安全列表是完美的吗?

常源
2023-03-14

下面是在动作2中使用来自C++并发的锁的线程安全列表的示例源代码。

template<typename T>
class threadsafe_list
{
    struct node
    {
        std::mutex m;
        std::shared_ptr<T> data;
        std::unique_ptr<node> next;
        node():
            next()
        {}
        node(T const& value):
            data(std::make_shared<T>(value))
        {}
    };
    node head;
public:
    threadsafe_list()
    {}
    ~threadsafe_list()
    {
        remove_if([](node const&){return true;}); // (1) (2)
    }
    threadsafe_list(threadsafe_list const& other)=delete;
    threadsafe_list& operator=(threadsafe_list const& other)=delete;
    void push_front(T const& value)
    {
        std::unique_ptr<node> new_node(new node(value));
        std::lock_guard<std::mutex> lk(head.m);
        new_node->next=std::move(head.next);
        head.next=std::move(new_node);
    }
    template<typename Function>
    void for_each(Function f)
    {
        node* current=&head;
        std::unique_lock<std::mutex> lk(head.m);
        while(node* const next=current->next.get())
        {
            std::unique_lock<std::mutex> next_lk(next->m);
            lk.unlock();
            f(*next->data);
            current=next;
            lk=std::move(next_lk);
        }
    }
    template<typename Predicate>
    std::shared_ptr<T> find_first_if(Predicate p) // (3)
    {
        node* current=&head;
        std::unique_lock<std::mutex> lk(head.m);
        while(node* const next=current->next.get())
        {
            std::unique_lock<std::mutex> next_lk(next->m);
            lk.unlock();
            if(p(*next->data))
            {
                return next->data;
            }
            current=next;
            lk=std::move(next_lk);
        }
        return std::shared_ptr<T>();
    }
    template<typename Predicate>
    void remove_if(Predicate p)
    {
        node* current=&head;
        std::unique_lock<std::mutex> lk(head.m);
        while(node* const next=current->next.get())
        {
            std::unique_lock<std::mutex> next_lk(next->m);
            if(p(*next->data)) // (2) - 1
            {
                std::unique_ptr<node> old_next=std::move(current->next);
                current->next=std::move(next->next);
                next_lk.unlock();
            }
            else
            {
                lk.unlock();
                current=next;
                lk=std::move(next_lk);
            }
        }
    }
};

我理解这段代码的工作原理。但我不认为这个代码是完美的。我在这三个地方做了记号。

(1)析构函数中的remove_if确有必要?节点中的每个数据都使用智能指针。所以我不认为析构函数必须移除列表中的元素。你的意见呢?

(2)即使使用remove_if,lambda函数的参数[](节点const&){...}看起来怪怪的。我认为lambda函数应该是[](T const&){...}这样的函数。这是因为如果使用节点类型,在点(2)-2处,它使用*next->data作为参数,所以从T到node的隐式类型转换发生在node构造函数中,这是冗余。如果我们使用T类型,就不必进行隐式转换。

(3)find_first_if返回shared_ptr而不是复制的T值,这给并发问题提供了机会。我会解释更多。如果用户使用find_first_if获得数据的shared_ptr,那么即使在修改节点时也可以通过该指针访问数据。这是安全的行动吗?我不这么认为。我的建议是,它应该返回T的复制值,这将导致T find_first_if(谓词p){...}。我说的对吗?

共有1个答案

贺华容
2023-03-14

>

  • 这实际上是必要的,不是为了防止泄漏,而是为了防止堆栈溢出。由于节点彼此拥有,因此每个节点的析构函数将递归调用以下所有节点的析构函数。因此破坏一个长列表将很容易炸毁堆栈。

    是的,这是不正确的,您应该查阅谓词的文档。更好的方法是使用C++14[](const auto&){return true;},但由于缺少make_unique,代码可能只使用C++11编写。

    是的,公开的数据不再受任何锁的保护。复制可能会很贵,而仅移动的类型呢?同样,这应该在方法中适当地记录下来。

  •  类似资料:
    • 问题内容: 我注意到,通常建议使用具有多个线程的队列,而不是列表和。这是因为列表不是线程安全的,还是出于其他原因? 问题答案: 列表本身是线程安全的。在CPython中,GIL防止对它们的并发访问,而其他实现则请小心地为它们的列表实现使用细粒度锁或同步数据类型。但是,虽然列表本身不会因尝试并发访问而损坏,但列表的数据不受保护。例如: 如果另一个线程做同样的事情,则不能保证实际上将增加1,因为这不是

    • 我们正在尝试使用以下查询将数据流式传输到postgres 11: 基本上“在表中插入记录,如果它已经存在 - 我们希望将此查询连接到消息队列,并在多个实例中的高并发环境中运行它。使用此查询,可能会从不同的连接访问同一行。对我们来说,只有具有最高交付时间戳的项目最终才能进入表是至关重要的 根据文件: 在冲突时,DO UPDATE保证原子插入或更新结果;如果没有独立的错误,那么即使在高并发的情况下,这

    • 我读过很多关于Servlet和threadsafe的文章--我知道,“Servlet容器只加载和实例化每个Servlet一次……”。但是,如果我创建抽象类extends Servlet,它有一个用参数处理的方法,那么在后代中使用这个threadsafe吗?

    • 问题内容: 我知道文档说明该对象是线程安全的,但这是否意味着从所有方法对其进行的所有访问都是线程安全的?因此,如果我一次从多个线程中调用它,并且一次在同一实例上调用它,会不会发生什么不好的事情? 问题答案: 快速答案是肯定的,它们是线程安全的。但是不要让它在那里… 首先,一个小的内部管理是一个接口,任何不是线程安全的实现都将破坏书面合同。您包括的链接是指,它具有一定的灵巧性。 您包含的链接引起了一

    • 我正在修改一个Java服务器软件。整个应用程序是单线程的。我的一个更改需要很多时间,所以我决定异步执行,以避免冻结主线程。 问题是:这种锁在Java中的最佳实现是什么?例如,我是否应该使用亲自完成。 编辑:看看我当前实现的答案。

    • 此">答案提供了对IntStream进行分区的实现: 但是它的编辑提到这个实现不是线程安全的。然而,据我所知,收集器创建了一个单独的