【问题标题】:Design Problem: Thread safety of std::map设计问题:std::map 的线程安全
【发布时间】:2011-08-16 13:16:25
【问题描述】:

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

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

【问题讨论】:

  • "std::map to implementation my local hash table" --> 你的意思是使用std::map 作为桶吗?

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


【解决方案1】:

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

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

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

读取器/写入器

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

自己的树

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

跳过列表

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

Skip List 建立在这种优势之上,但增强了结构以提供 O(log N) 密钥访问。

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

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

持久性

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

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

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

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

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

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


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

【讨论】:

  • Matthieu M:如果二叉树是一棵红黑树,那么重新平衡操作的数量永远不会超过恒定数量,尽管重新着色操作的数量可能是对数。可以以这样一种方式实现它们,即在重新着色发生时其他线程可以读取,只要没有其他线程更新树。只有实际的重新平衡必须是独占的,即使这样也只需要每个子树。
  • @Jorgen:感谢您的确切影响。
【解决方案2】:

您确实需要将 find 放在关键部分,但您可能需要两个不同的锁,一个用于写入,一个用于读取。写锁是独占的,但如果没有线程持有写锁,则多个线程可以同时读取而不会出现问题。

这样的实现适用于大多数 STL 实现,但它不符合标准。 std::map 通常是使用 red-black tree 实现的,它在读取元素时不会改变。如果映射是使用splay tree 实现的,则树会在查找期间发生变化,并且一次只能读取一个线程。

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

【讨论】:

  • 我部分认同你的论点,但读锁也将在多个线程之间共享。不知何故,我需要维护多少线程正在执行查找操作,以便可以释放读锁。此外,只有在所有读取完成后才能获取写锁,因此它们不会受到影响。在某种程度上,插入/删除/查找所有操作都需要等待同一个锁。
  • @Ashish: 但是你可以有多个并发的find 和一个写/读锁,而一个锁你一次只能有一个find
【解决方案3】:

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

【讨论】:

    【解决方案4】:

    是的 - 您需要将插入、删除和查找放在关键部分。有一些技术可以同时进行多个查找。

    【讨论】:

    • 能否请您详细说明启用多个查找的技术?
    【解决方案5】:

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

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

    【讨论】:

    • 其实不太一样。另一个问题是关于让多个线程读取一个永远不会改变的映射。这个问题是关于线程安全的并发读取和修改。
    • 呃,对,问题不一样,但答案解释了从多个线程访问映射是安全的情况和方式,以及不安全的情况和方式,以及也回答了这个问题。它还提供了一个链接,其中包含有关线程安全的更详细说明。将改写我的答案
    • 我认为从这个答案中学到的更重要的教训是线程安全取决于您正在使用的 stl 的实现
    • 问题不一样。就我而言,地图可以随时修改。
    • 因此查找操作可以与插入/删除操作并发,也可以与任何其他查找操作同时进行。
    猜你喜欢
    • 1970-01-01
    • 2013-02-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-24
    • 2010-12-23
    • 1970-01-01
    相关资源
    最近更新 更多