防止共享哈希表中的数据竞争

Posted

技术标签:

【中文标题】防止共享哈希表中的数据竞争【英文标题】:preventing data races in shared hash table 【发布时间】:2021-12-21 20:05:52 【问题描述】:

如果这是重复的,我很抱歉,但我搜索的只是找到不适用的解决方案:

所以我有一个哈希表,我希望多个线程同时读取和写入该表。但是在以下情况下如何防止数据竞争:

线程写入与另一个相同的哈希 写入正在读取的哈希的线程

编辑: 如果可能的话,因为这个哈希需要非常快,因为它被非常频繁地访问,有没有办法锁定两个竞赛线程,只有当它们访问哈希表的相同索引时?

【问题讨论】:

【参考方案1】:

所以你需要基本的线程同步还是什么?您必须在读写函数中使用 mutex、lock_guard 或其他一些线程同步机制。在 cppreference.com 你有标准库的文档。

【讨论】:

【参考方案2】:

避免数据竞争的最可靠和最合适的方法是使用互斥锁序列化对哈希表的访问;即每个线程在对哈希表执行任何操作(读取或写入)之前都需要获取互斥锁,并在完成后释放互斥锁。

不过,您可能正在寻找的是实现一个无锁哈希表,但确保正确的多线程行为没有锁是极其困难的,如果您在实现这样的事情所需的技术水平,你不需要在 *** 上询问它。所以我强烈建议你要么坚持使用序列化访问方法(它适用于 99% 的软件,并且可以在没有深入了解 CPU、缓存架构、RAM、操作系统、调度程序的情况下正确实现、优化器、C++ 语言规范等),或者如果您必须使用无锁数据结构,您可以从信誉良好的来源找到一个预制的数据结构来使用,而不是尝试自己动手。事实上,即使您想自己动手,也应该从查看工作示例的源代码开始,了解他们在做什么以及为什么要这样做。

【讨论】:

【参考方案3】:

我以前回答过这个问题的变体。请阅读我的previous answer 关于这个话题。

许多人尝试实现线程安全的集合类(列表、哈希表、映射、集合、队列等...)但失败了。或者更糟糕的是,失败了,不知道,但还是发货了。

构建线程安全哈希表的一种简单方法是从现有哈希表实现开始,并为所有公共方法添加互斥锁。你可以想象一个假设的实现是这样的:

// **THIS IS BAD**

template<typename K, typename V>
class ThreadSafeMap

private:
    std::map<K,V> _map;
    std::mutex _mutex;

public:

    void insert(const K& k, const V& v)
    
        std::lock_guard lck(_mutex);
        _map[k] = v;
    

    const V& at(const K& key)
    
        std::lock_guard lck(_mutex);
        return _map.at(k);
    

    // other methods not shown - but are essentially a repeat of locking a mutex
    // before accessing the underlying data structure

;

在上面的例子中,std::lock_guardlck变量被实例化时锁定互斥锁,而lock_guard的析构函数会在lck变量超出范围时释放互斥锁

而且在一定程度上,它是线程安全的。但是当你开始以复杂的方式使用上述数据结构时,它就崩溃了。

哈希表上的事务通常是多步操作。例如,表上的整个应用程序事务可能是查找一条记录,并在成功返回它后,更改记录指向的某个成员。

所以想象一下我们在不同的线程中使用了上面的类,如下所示:

 ThreadSafeMap g_map<std::string, Item>;

 // thread 1
 Item& item = g_map.at(key);
 item.value++;
 

 // thread 2
 Item& item = g_map.at(key);
 item.value--;

 // thread 3
 g_map.erase(key);
 g_map[key] = newItem;

很容易认为上述操作是线程安全的,因为哈希表本身是线程安全的。但他们不是。线程 1 和线程 2 都试图访问锁外的同一个项目。线程 3 甚至试图替换其他两个线程可能引用的记录。这里有很多未定义的行为。

解决方案?坚持使用单线程哈希表实现并在应用程序/事务级别使用互斥锁。更好:

 std::unordered_map<std::string, Item> g_map;
 std::mutex g_mutex;

 // thread 1
 
   std::lock_guard lck(g_mutex);
   Item& item = g_map.at(key);
   item.value++;
 
 

 // thread 2
 
   std::lock_guard lck(g_mutex);
   Item& item = g_map.at(key);
   item.value--;
 

 // thread 3
 
   std::lock_guard lck(g_mutex);
   g_map.erase(key);
   g_map[key] = newItem;
 

底线。不要只是在低级数据结构上粘贴互斥锁和锁,并宣称它是线程安全的。在调用者期望对哈希表本身执行其操作集的级别上使用互斥锁和锁。

【讨论】:

谢谢,只有当两个线程访问同一个索引时,我才能锁定互斥锁吗?我的程序是一个国际象棋引擎,每秒访问这个哈希数千次。为无论如何都不会竞争的线程锁定整个表可能是无效的 在您测量之前,您不应该假设会有性能问题。我不知道您的访问模式或数据结构,但您始终可以将互斥锁与表中的每个单独值关联。 谢谢,我想我会尝试存储一个互斥锁,每个都有索引! 等等,网上说std::mutex的大小是80字节!任何其他可能更节省内存的方式?我可能会为每 N 个索引分配一个互斥锁以节省内存,同时减少线程的互斥锁等待时间

以上是关于防止共享哈希表中的数据竞争的主要内容,如果未能解决你的问题,请参考以下文章

数据结构与算法实例(哈希表实现)

数据保险库:临时表中的哈希键 - 高级

如何使用 glib 处理哈希表中的冲突

哈希表中的查找

B树与哈希表

C ++:指针作为哈希表中的键