如何以线程安全的方式使用`std::unordered_map`?

Posted

技术标签:

【中文标题】如何以线程安全的方式使用`std::unordered_map`?【英文标题】:How to use `std::unordered_map` in a thread-safe way? 【发布时间】:2021-07-12 19:07:37 【问题描述】:

我做了什么

我的想法是包装std::unordered_map,并为每个人锁定它 手术。它看起来像这样(我将只留下一个操作 为了不使问题混乱):

template<class Key, class T>
class thread_safe_unordered_map 
   public:
    T &operator[](const Key &key) 
        std::lock_guard<std::mutex> lockm_mutex;
        return m_underlying[key];
    

   private:
    std::unordered_map<Key, T> m_underlying;
    std::mutex                 m_mutex;
;

但是后来我想到了,现在我不知道如何正确地做到这一点。

想象一下这种情况。

线程 1: 调用 operator[] 进行写入线程 2: 调用 operator[] 进行读取

两者都各司其职。现在,因为 operator[] 返回一个引用,并且它们都 将根据相同的参考采取行动,我认为这仍然可能是一场数据竞赛。 是这样吗?

我现在的想法

因为this:

所有的 const 成员函数都可以被不同的线程同时调用 同一个容器。此外,成员函数 begin()、end()、 rbegin()、rend()、front()、back()、data()、find()、lower_bound()、 upper_bound()、equal_range()、at() 以及,除了在关联容器中, operator[],出于线程安全的目的,表现为 const。

我有这个问题。在线程上使用at() 是否正确,并且 被锁定的是映射值?

【问题讨论】:

返回非常量引用与封装相反。你是对的,锁无助于保护对同一内存的并发读写导致数据竞争 更糟糕的是,新获取的引用可能随时变成悬空引用,而不会发出警告。如果幸运的话,您将能够对撞击坑进行一些取证调试,以找出问题所在。 您是否曾经从多线程区域的地图中删除元素?如果没有,你应该没问题。一旦一个元素被插入到一个无序映射中,它的地址应该保持不变。无论如何,这种方法效率低下。有外部库提供的并发哈希表,例如 Intel TBB 或 Facebook Folly (IIRC)。此外,正如所指出的,元素的更新可能会导致数据竞争。 不,没有被删除。有些是在开始时插入的,之后只有一个线程可以修改现有元素,其余的只能从中读取。谢谢你的建议,我会看看你告诉我的图书馆。 @DanielLangr 锁定每个操作通常是不够的,因为很多时候您想要原子地执行多个操作(例如,删除一个条目然后插入另一个条目,而没有任何其他线程有可能看到这两个操作之间的中间状态)。因此,更好的方法是将锁定排除在数据结构之外,并在更高级别完成,代码完全了解它试图以原子方式完成的任务。 【参考方案1】:

现在,因为 operator[] 返回一个引用,并且它们都将作用于同一个引用,我认为这仍然可能是数据竞争。是这样吗?

这是正确的假设 T 不是线程安全类型。

这不是容器操作中的数据竞争,而是T类型对象的使用中的数据竞争。

我有这个问题。在线程上使用 at() 是否正确

如果你只调用 const 限定的at,那么你永远不会得到一个非常量引用,因此不能修改任何元素的值。如果您不修改这些值,那么您就没有数据竞争。

如果您使用非 const 限定 at,那么在所描述的数据竞争方面没有区别。

附:您会发现您将无法拥有任何同步的非常量成员函数,因为锁定互斥体需要修改。您可以使用mutable 解决此问题。

【讨论】:

以上是关于如何以线程安全的方式使用`std::unordered_map`?的主要内容,如果未能解决你的问题,请参考以下文章

如何以线程安全的方式使用`std::unordered_map`?

如何以编程方式证明 StringBuilder 不是线程安全的?

C++ - 如何以独立于平台、线程安全的方式以用户首选的日期/时间语言环境格式格式化文件的最后修改日期和时间

以线程安全的方式使用 DbContext 进行异步搜索

并行流是不是以线程安全的方式处理上游迭代器?

有效地以线程安全的方式使用BufferedImage?