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

时间:2011-08-16 13:16:26

标签: c++ multithreading stl thread-safety posix

我使用std :: map来实现我的本地哈希表,它将由多个线程同时访问。 我做了一些研究,发现std :: map不是线程安全的。 所以我将在地图上使用互斥锁进行插入和删除操作。 我打算使用单独的互斥锁,每个映射条目一个,以便可以单独修改它们。

我是否还需要将查找操作放在关键部分? 查找操作会受插入/删除操作的影响吗? 有没有比使用可以处理所有事情的std :: map更好的实现?

5 个答案:

答案 0 :(得分:5)

二叉树不是特别适合多线程,因为重新平衡可以在树范围的修改中退化。此外,全局互斥体将非常负面地访问性能。

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

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

<强>读/写

您可能希望使用Reader / Writer Mutex而不是常规Mutex。这意味着并行化读取,而写入保持严格顺序。

自有树

您还可以构建自己的红黑或AVL树。通过每个节点使用Reader / Writer Mutex扩充树结构。这允许您仅阻止树的部分,而不是整个结构,即使在重新平衡时也是如此。 例如插入密钥足够远可以是平行的。

跳过列表

链接列表更适合并发操作,因为您可以轻松隔离已修改的区域。

Skip List建立在这种力量的基础上,但增加了结构,以便通过密钥提供O(log N)访问。

行走列表的典型方法是使用 hand hand 习惯用法,即在释放当前节点之一之前获取下一个节点的互斥锁。跳过列表添加第二个维度,因为您可以在两个节点之间潜水,从而释放它们(并让其他步行者超越您)。

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

<强>持久性

另一个有趣的部分是持久性(或半持久性)数据结构的概念,通常在函数式编程中找到。二进制搜索树特别适合它。

基本思想是永远不要更改节点(或其内容)。你这样做是通过共享一个可变的 head 来指向更高版本的。

  • 阅读:您复制当前头部,然后无忧无虑地使用它(信息是不可变的)
  • 要写入:您将在常规树中修改的每个节点都会被复制并修改副本,因此每次都会重建树的一部分(直到根),并更新 head 指向新的根。有一些有效的方法可以在树下降时进行重新平衡。 写入是顺序的

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

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

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


我不知道C ++中并发Skip List或并发半持久二进制搜索树的任何实现。

答案 1 :(得分:1)

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

答案 2 :(得分:1)

是 - 您需要在关键部分插入,删除和查找。有一些技术可以同时启用多个查找。

答案 3 :(得分:1)

你将需要将find放在一个关键部分,但你可能想要两个不同的锁,一个用于写入,一个用于读取。写锁是独占的,但如果没有线程保持写锁,则几个线程可以同时读取而没有任何问题。

此类实现适用于大多数STL实现,但它不符合标准。 std::map通常使用red-black tree来实现,splay tree在读取元素时不会更改。如果地图是使用{{3}}实现的,那么树在查找期间会发生变化,并且一次只能读取一个线程。

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

答案 4 :(得分:0)

从我所看到的,这里已经回答了类似的问题,答案也包括对这个问题的解释,以及更详细地解释线程安全性的链接。

Thread safety of std::map for read-only operations