mold 内置 TBB 并发哈希容器:concurrent_unordered_multimap 查找(Lookup)接口深度解析
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
mold 链接器在构建产物中内置了 Intel oneAPI TBB 并发容器作为第三方依赖,其中oneapi::tbb::concurrent_unordered_multimap是一个支持并发插入、查找与遍历但不支持并发删除的无序关联容器,且允许多个元素拥有等价键。本文以 TBB 规范文档中的 Lookup 章节为主体,逐一解析该容器四个查找接口count、find、contains、equal_range的语义、重载规则与并发安全性,并结合 容器基类实现 与 一致性测试 剖析其底层原理。读完本文,你将能在多线程场景下正确、高效地使用这些查找接口,并理解透明哈希(heterogeneous lookup)重载的启用条件。
查找接口的并发安全承诺
依据规范文档 lookup.rst 的开篇声明,本节描述的所有方法都可以相互并发执行,也可以与所有并发安全的修改操作(modifiers)以及容器的遍历操作并发执行。这一承诺是该容器的核心价值:在多线程环境下,读者线程无需加锁即可执行查找,写者线程可同时进行插入,二者互不阻塞。
需要强调的是,规范同时指出该容器不支持并发删除(“supports concurrent insertion, lookup, and traversal, but does not support concurrent erasure”,见 concurrent_unordered_multimap.rst 的概述段落,实际路径为 concurrent_unordered_multimap.rst)。删除操作属于unsafe_modifiers(不安全修改器)范畴,unsafe_modifiers.rst 中给出了详细说明——在调用不安全修改器期间,不允许其他线程并发访问容器。因此,Lookup 接口的并发安全保证适用于“读取 + 并发安全写入”的组合,使用时必须把erase一类不安全操作排除在并发访问窗口之外。
类模板与类型别名回顾
concurrent_unordered_multimap定义在头文件<oneapi/tbb/concurrent_unordered_map.h>中(concurrent_unordered_map.h),模板形参为:
template <typename Key, typename T, typename Hash = std::hash<Key>, typename KeyEqual = std::equal_to<Key>, typename Allocator = tbb_allocator<std::pair<const Key, T>>> class concurrent_unordered_multimap;其中key_type为Key,mapped_type为T,value_type为std::pair<const Key, T>。从源码结构看,concurrent_unordered_multimap通过concurrent_unordered_map_traits<Key, T, Hash, KeyEqual, Allocator, true>继承自基类concurrent_unordered_base(concurrent_unordered_map.h),其第 6 个模板参数true表示“允许多重映射(allow_multimapping)”——这正是 multimap 允许键重复的开关。所有 Lookup 接口本身均由基类实现,派生类仅通过using base_type::...继承暴露,因此本节语义对concurrent_unordered_map(单键)同样成立,只是 multimap 特有的“多个等价键”行为需要按下面的说明处理。
接口一:count —— 统计等价键元素个数
size_type count( const key_type& key ); template <typename K> size_type count( const K& key );返回语义:容器中键与key等价(equivalent)的元素个数。对 multimap 而言,由于允许多个元素共享同一键,返回值可能大于 1。
基类实现位于 _concurrent_unordered_base.h:
size_type count( const key_type& key ) const { return internal_count(key); } template <typename K> typename std::enable_if<is_transparent<K>::value, size_type>::type count( const K& key ) const { return internal_count(key); }注意两点实现细节:
- 模板重载通过
std::enable_if<is_transparent<K>::value, ...>约束,这与规范中“仅当hasher::transparent_key_equal合法且表示一个类型时才参与重载决议”的描述完全一致(is_transparent即对透明性特征进行检测的别名)。 internal_count(_concurrent_unordered_base.h)在 multimap 模式下直接复用equal_range并计算区间距离:
template <typename K> size_type internal_count( const K& key ) const { if (allow_multimapping) { // TODO: consider reimplementing the internal_equal_range with elements counting to avoid std::distance auto eq_range = equal_range(key); return std::distance(eq_range.first, eq_range.second); } else { return contains(key) ? 1 : 0; } }源码中的 TODO 注释也提示:multimap 的 count 走的是“查区间再数距离”的路径,成本与等价键数量成正比;而单键 map 走的是contains快速路径,复杂度为 O(1) 期望。从源码结构可以推断,如果读者只需要判断“是否存在”而不关心数量,contains会比count更廉价。
接口二:find —— 定位等价键元素
iterator find( const key_type& key ); const_iterator find( const key_type& key ) const; template <typename K> iterator find( const K& key ); template <typename K> const_iterator find( const K& key ) const;返回语义:返回指向键与key等价元素的迭代器;若不存在则返回end()。关键约定:当存在多个等价键元素时,找到哪一个元素是未指定的(unspecified)——实现没有义务返回“第一个”或“最后一个”,调用方不应依赖返回值相对于其他等价键的先后位置。
基类实现(_concurrent_unordered_base.h)通过非 const 版本统一调用internal_find,const 版本则const_cast后复用同一逻辑,避免代码重复。底层internal_find(_concurrent_unordered_base.h)体现了该容器“split-ordered list(分段有序链表)”的核心数据结构:
template <typename K> value_node_ptr internal_find( const K& key ) { sokey_type hash_key = sokey_type(my_hash_compare(key)); sokey_type order_key = split_order_key_regular(hash_key); node_ptr curr = prepare_bucket(hash_key); while (curr != nullptr) { if (curr->order_key() > order_key) { // 若节点有序键已大于目标,则目标必然不在表中 return nullptr; } else if (curr->order_key() == order_key && my_hash_compare(traits_type::get_key(static_cast<value_node_ptr>(curr)->value()), key)) { // 有序键相同并不代表元素相等,仍需调用 key 比较函数确认 return static_cast<value_node_ptr>(curr); } curr = curr->next(); } return nullptr; }查找过程分三层:先对键做哈希并换算成“分段有序键”(split-order key);随后prepare_bucket确保目标桶对应的链表段已初始化;最后沿链表线性推进——有序键大于目标时提前终止(借助有序性剪枝),有序键相等时再用key_equal做最终比对。代码注释特别强调:“有序键相同并不意味着找到了元素”,必须经过键等价性比较才能确认命中,这保证了即使不同键哈希碰撞到同一有序键,也不会产生误报。
接口三:contains —— 是否存在等价键
bool contains( const key_type& key ) const; template <typename K> bool contains( const K& key ) const;返回语义:容器中至少存在一个键与key等价的元素时返回true,否则返回false。它不关心等价键的数量,因此语义上等价于find(key) != end()。
基类实现(_concurrent_unordered_base.h)确实就是这么做的:
bool contains( const key_type& key ) const { return find(key) != end(); } template <typename K> typename std::enable_if<is_transparent<K>::value, bool>::type contains( const K& key ) const { return find(key) != end(); }在并发场景下,contains通常比count更受青睐:它只需找到第一个匹配即可提前返回,且语义清晰。一致性测试中也能看到该接口的直接使用,例如 concurrent_unordered_common.h 在遍历校验时以if (!c.contains(Value<UnorderedType>::key(*it)))断言每个元素键都仍在容器中。
接口四:equal_range —— 获取等价键区间
std::pair<iterator, iterator> equal_range( const key_type& key ); std::pair<const_iterator, const_iterator> equal_range( const key_type& key ) const; template <typename K> std::pair<iterator, iterator> equal_range( const K& key ); template <typename K> std::pair<const_iterator, const_iterator> equal_range( const K& key ) const;返回语义:若存在至少一个键与key等价的元素,返回迭代器对{f, l},其中f指向第一个等价键元素,l指向最后一个等价键元素之后的那个元素(即经典左闭右开区间[f, l));若不存在任何等价键元素,返回{end(), end()}。对 multimap 而言,这是枚举某个键下全部关联值的标准手段。
基类实现委托给internal_equal_range(_concurrent_unordered_base.h),其逻辑与internal_find同构:先定位到第一个匹配节点,然后沿链表继续推进,只要后续节点不是哨兵节点且键仍与目标等价就继续后移(循环条件中的allow_multimapping && last != nullptr && !last->is_dummy() && key_equal(..., key)保证了 multimap 模式下能跨越全部等价键),最终返回{first, first_value_node(last)}。正因为要跨越所有等价键,equal_range的复杂度与等价键数量线性相关。
典型用法(遍历某键下的全部映射值):
oneapi::tbb::concurrent_unordered_multimap<std::string, int> table; // ... 并发插入若干 ("key", v) 对 ... auto [first, last] = table.equal_range("key"); for (auto it = first; it != last; ++it) { // it->first 恒等于 "key",it->second 为各关联值 process(it->second); }由于 multimap 不保证等价键之间的顺序,[f, l)区间内元素的先后排列同样是未指定的,业务逻辑不应依赖该顺序。
透明键查找(Heterogeneous Lookup):模板重载的启用条件
四个接口都提供了以template <typename K>声明的透明重载。这类重载允许用与key_type不同类型的键执行查找——例如用std::string_view或const char*去查std::string键的容器,从而避免临时构造std::string的开销。但规范明确规定:该重载仅当限定名hasher::transparent_key_equal合法且表示一个类型时才参与重载决议。
在 TBB 实现中,这一机制的判定链条如下:
- _containers_helpers.h 中的特征类
has_transparent_key_equal专门检测Hash::transparent_key_equal是否存在:
template <typename Key, typename Hasher, typename KeyEqual, typename = void> struct has_transparent_key_equal : std::false_type { using type = KeyEqual; }; template <typename Key, typename Hasher, typename KeyEqual, typename> struct has_transparent_key_equal<Key, Hasher, KeyEqual, tbb::detail::void_t<typename Hasher::transparent_key_equal>> : std::true_type { // 并静态断言 transparent_key_equal::is_transparent 必须合法 static_assert(comp_is_transparent<type>::value, "Hash::transparent_key_equal::is_transparent is not valid or does not denote a type."); };_hash_compare.h 中的
hash_compare依据该特征把key_equal解析为has_transparent_key_equal::type,并对外提供透明版本的重载运算符operator()(K&& key)与operator()(K1&&, K2&&)——这些重载同样用std::enable_if<is_transparent_hash::value, ...>约束,确保只有哈希器声明了透明性时才参与重载。基类查找接口(前述
find、count、contains、equal_range的模板版本)再用std::enable_if<is_transparent<K>::value, ...>收口。
启用方式:自定义哈希器时,在哈希器内部提供一个名为transparent_key_equal的嵌套类型,并让该类型带is_transparent标记。典型写法:
struct string_hash { using transparent_key_equal = std::equal_to<>; // 声明透明等价性 std::size_t operator()(std::string_view s) const { return std::hash<std::string_view>{}(s); } std::size_t operator()(const std::string& s) const { return std::hash<std::string>{}(s); } }; oneapi::tbb::concurrent_unordered_multimap<std::string, int, string_hash> table; // 直接以 const char* / std::string_view 查找,避免构造临时 std::string auto n = table.count("literal-key"); if (table.contains(std::string_view("sv-key"))) { /* ... */ }若哈希器未声明transparent_key_equal,模板重载会被 SFINAE 掉,调用会回落到key_type版本,此时传入异构键会经历隐式转换。规范中“只参与重载决议(only participates in overload resolution)”的措辞与std::enable_if的 SFINAE 约束在语义上完全对应。
并发查找的正确姿势与易错点
综合规范与实现,使用这四个查找接口时有几点值得注意:
- 返回值的时效性:所有 Lookup 接口都是即时快照。
find返回的迭代器指向的元素可能随后被其他线程通过并发安全的修改器更新(节点按值存储),但不会被删除——因为删除必须走unsafe_modifiers,而那种操作要求独占访问,不允许与查找并发。因此只要遵守“不安全修改器独占”的约定,查找迭代器就不会悬空。 - 不要依赖等价键之间的返回位置:
find在多等价键时返回“哪一个”是未指定的;equal_range区间内顺序也是未指定的。 countvscontains:只关心“有没有”时优先contains(底层即find != end());需要精确数量时用count(multimap 下走区间距离计算)。- 异构键查找的前提:想用
std::string_view/const char*这类键查找,必须让哈希器声明transparent_key_equal,否则模板重载不参与决议。
这些语义在仓库的测试套件中有据可查:一致性测试 conformance_concurrent_unordered_map.cpp 与公共测试头 concurrent_unordered_common.h 覆盖了contains遍历校验、桶遍历计数等场景;而透明查找特征与static_assert的检查路径可在 _containers_helpers.h 中直接阅读,便于验证自定义哈希器的声明是否符合要求。
小结
concurrent_unordered_multimap的 Lookup 章节定义了四个语义清晰、可并发安全的查找接口:count统计等价键数量、find定位单个等价键元素、contains判定存在性、equal_range获取完整的等价键区间。它们都提供基于hasher::transparent_key_equal门控的异构键重载,底层由 split-ordered list 与分段桶结构支撑,查找过程借助有序键剪枝与键等价性二次比对保证正确性。在 mold 所集成的这套 TBB 容器中,理解这些接口的语义边界(尤其是“多等价键时结果未指定”与“删除需独占”)是在多线程环境中写出正确代码的前提。
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考