设计问题:std::map的线程安全性

Design Problem: Thread safety of std::map

本文关键字:线程 安全性 map std 问题      更新时间:2023-10-16

我使用std::map来实现我的本地哈希表,它将由多个线程同时访问。我做了一些研究,发现std::map不是线程安全的。因此,我将使用互斥锁对映射进行插入和删除操作。我计划使用单独的互斥锁,每个map条目对应一个互斥锁,这样它们就可以被独立修改。

我需要把查找操作也放在临界区下吗?查找操作是否会受到插入/删除操作的影响?还有比使用std::map更好的实现吗?

二叉树并不特别适合多线程,因为重新平衡可能在树范围的修改中退化。此外,全局互斥锁会对性能产生负面影响。

我强烈建议使用已经编写好的线程安全容器。例如,Intel TBB包含concurrent_hash_map .

如果您希望学习,这里有一些关于构建并发排序关联容器的提示(我相信完整的介绍不仅超出了我的能力范围,而且在这里也不合适)。

读/写

比起普通的互斥锁,你可能想使用读写互斥锁。这意味着并行的读,而写仍然严格顺序。

<<p> 的树/strong>

您还可以构建自己的红黑树或AVL树。通过在每个节点上增加一个读写互斥来扩展树结构。这允许您只阻塞树的部分,而不是整个结构,即使在重新平衡时也是如此。eg键间距足够远的插入可以并行。

跳跃表

链表更适合并发操作,因为您可以很容易地隔离修改的区域。

跳跃表建立在这种强度的基础上,但增加了结构,以提供O(log N)键访问。

遍历列表的典型方法是使用hand over hand习惯用法,也就是说,在释放当前节点的互斥锁之前,先获取下一个节点的互斥锁。跳跃列表添加了第二个维度,因为你可以在两个节点之间潜水,从而释放它们(并让其他步行者走在你前面)。

的实现比二叉搜索树简单得多。

持续

另一个有趣的部分是持久(或半持久)数据结构的思想,它经常出现在函数式编程中。二叉搜索树特别适合它。

基本思想是,一旦节点存在,就永远不要更改节点(或其内容)。你可以通过共享一个可变的来实现,它将指向以后的版本。

  • To Read:你复制当前的头,然后使用它不用担心(信息是不可变的)
  • To Write:你要在常规树中修改的每个节点都被复制,副本被修改,因此你每次都重建树的一部分(直到根),并更新指向新的根。有一些有效的方法可以在树的下行过程中进行再平衡。顺序写入

主要优点是总有一个版本的地图可用。也就是说,即使另一个线程正在执行插入或删除操作,也始终可以读取。此外,由于read访问只需要单个并发的读取(当复制根指针时),它们几乎是无锁的,因此具有出色的性能。

引用计数(内在)是这些节点的朋友。

注意:树的副本非常便宜:)


我不知道c++中任何并发跳跃表或并发半持久二叉搜索树的实现。

您确实需要将find放在临界区中,但是您可能希望有两个不同的锁,一个用于写,一个用于读。写锁是排他的,但是如果没有线程持有写锁,多个线程可以并发地读,没有问题。

这样的实现可以与大多数STL实现一起工作,但是它不符合标准。std::map通常使用红黑树实现,该树在读取元素时不会改变。如果map是用一棵树来实现的,那么树将在查找过程中改变,并且一次只有一个线程可以读取。

对于大多数用途,我建议使用两个锁。

是的,如果插入或删除导致重新平衡,我相信find也可能受到影响。

是-您需要将插入,删除和查找放在临界区。有一些技术可以同时实现多个查找。

据我所知,这里已经回答了一个类似的问题,并且答案还包括对该问题的解释,以及更详细地解释线程安全的链接。

只读操作std::map的线程安全性