设计问题:std::map的线程安全性
Design Problem: Thread safety of std::map
我使用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的线程安全性
- 类与私有变量的其他类之间的线程安全性
- 调用socket.remote_endpoint(提升 asio 库)线程安全性
- std::lock_guard 似乎提供了线程安全性,尽管作用域块
- C++中向量和列表的非写入成员函数的线程安全性
- 线程安全性和静态变量/成员功能
- 是仅使用get或toplown的原始类型的线程安全性的威胁
- 提高 ASIO stream_descriptor和事件 FD 线程安全性
- OpenMP中树结构的线程安全性
- 静态变量初始化的线程安全性
- 从另一个(非 qt)线程调用 QObject 方法的线程安全性?
- C 共享_ptr如何确保线程安全性
- 编写std::vector与普通数组的线程安全性
- C++标准库容器相对于所包含对象的线程安全性
- 读取和写入操作的线程安全性C++
- Qt库-静态成员函数的线程安全性
- QObject可重入性和线程安全性
- std::map中的线程安全性
- boost::asio io_service 和 std::containers 的线程安全性
- C++11 std:原子<T>复制构造函数的线程安全性
- Clang 线程安全性分析和线程角色