【发布时间】:2015-10-26 21:39:50
【问题描述】:
在 C++ 中实现 threadsafe ordered(note1) 映射/散列的最佳方式是什么?又名,一种快速查找数据结构(又名,不是队列),不同线程可以迭代,偶尔插入或删除元素而不干扰其他线程的活动?
std::map 不是线程安全的,它的操作是非原子的 - 尽管只有擦除会使迭代器失效
将每个函数包装在整个地图类中并不能解决问题 - 您可以有松散的迭代器指向一个被另一个线程擦除的节点。它应该锁定并阻止删除,直到当前线程是唯一引用它的线程,或者使用 UNIX 文件系统风格的“悬空但在删除时仍然有效的引用”方法
- tbb::concurrent_hash_map 被设计为线程安全的,但它的迭代器在删除其键时仍然无效。即使 Value 是智能指针,也不会保留数据,因为迭代器中的引用会丢失。
- 可以通过遍历键而不是迭代器来使用 tbb:concurrent_hash_map(可以将键查找为 O(1) 而不是像 std::map 那样的 O(log N)),但由于它是一个散列,它缺少与顺序相关的函数,如上界、下界等。关键的是,如果一个键在另一个线程中被擦除而悬空,没有明显的方法可以告诉代码跳回到哈希中离该键最近的元素。李>
- std::unordered_map 如果您的密钥通过存储桶访问功能被删除,则可能会尝试通过陪审团找到最近的元素。但是 std::unordered_map 不被认为是线程安全的(尽管它可能被包装为线程安全的)。 tbb::concurrent_hash_map 是线程安全的(受上述约束),但它不允许足够的存储桶访问权限来执行此操作。
- std::map,通过键而不是迭代器访问,如果键被另一个线程删除,则可以在映射中找到下一个最近的元素。但每次查找都是 O(log N) 而不是 O(1)。
- 另外,如前所述,std::map 是非原子的。它必须被包裹起来。使用键而不是迭代器还需要包装整个类,因为必须将通常采用或返回迭代器的所有函数更改为采用或返回键。
- 我读过的一个站点上的某个人说 std::map 在线程环境中无论如何都不能很好地工作(与哈希相比),因为线程不能很好地与树重新平衡一起使用,尽管我真的不知道为什么将是或者它是否普遍适用/准确。
你认为最好的方法是什么?
** 注1:当我写“ordered”时,我的意思是仅从“具有可以迭代的可靠排序”的角度来看,而不是“迭代必须按照键的顺序进行”。在我写完之后,我意识到我的几个用例确实关心迭代的顺序(大多数不关心)。但无论哪种方式,我都可以通过在 Value 类型中使用链表来判断正确的排序。只是更慢的丑陋和潜在的问题/疏忽......
** 注2:新思想。太糟糕的地图没有对其迭代器类型进行模板化......改变迭代器 std::map 的构建有多难?我之前一直在玩弄让迭代器成为引用计数(如 std::shared_ptr)的想法,但我一直错误地想用迭代器本身内部的辅助数据结构来实现引用计数,结果总是太丑陋了/缓慢/不切实际。但现在我突然想到,可以将引用计数包含在映射的键值对的值中。也就是说,每个值都将包括 A) 一个引用计数器(默认值 = 0),每个交互器在到达它时递增(operator=、operator++、operator-- 等)并在它离开时递减;和 B) 擦除功能设置的擦除标志 (default=false)。每当迭代器将引用计数器减为零时,如果设置了擦除标志,那么它将然后实际擦除它。
在我看来,虽然它会影响性能(额外的增量/减量/检查),但每次您想要逐步浏览结构时都必须进行完整的地图查找,这肯定没什么。谁能想到一个实用的方法来实现这个?
【问题讨论】:
-
顺序和散列不能一起工作 - 你需要选择一个。
-
不一定是哈希,根据问题。试图在哈希表上模仿强加的顺序(锁定 => 找到密钥 => 获取匹配的迭代器 => 递增迭代器 => 解锁;在密钥已被删除的情况下,搜索桶以模拟上限/下限)作为一种选择进行讨论。一个丑陋的选择。但到目前为止,我还没有想出任何漂亮的选项,只有几个不同的丑陋选项。所以如果你有更好的想法,请告诉我:)
-
@user416650 您的问题过于笼统和模糊,实际上无法获得简洁的答案。
-
您的地图有多大?除了正确的操作,您还需要什么其他要求?
-
@Nim 为什么?假设有足够的内存并且愿意忍受额外的插入/删除时间,那么没有什么能阻止您编写一个实现哈希和链表的类,以便在需要迭代时促进使用特定键的快速查找和有序查找。
标签: c++ multithreading c++11 stdmap tbb