TBB concurrent_unordered_multimap 非成员 swap 解析:OneTBB 容器规范与源码级实现
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
在多线程 C++ 项目中,concurrent_unordered_multimap是 Intel oneAPI Threading Building Blocks(oneTBB)提供的哈希式并发容器,常用于以同一键对应多个值的并行场景。本文以 oneTBB 容器规范中concurrent_unordered_multimap的 "Non-member swap" 条目为核心,结合仓库内 oneTBB 的实际头文件实现,讲清这个非成员swap函数的签名、noexcept规格、与成员swap的等价关系,以及底层交换时分配器(allocator)传播规则,帮助读者正确、安全地在并发容器上使用交换操作。
一、规范条目原文:签名与语义
oneTBB 规范文档(见 non_member_swap.rst)对该接口的定义非常精炼:
template <typename Key, typename T, typename Hash, typename KeyEqual, typename Allocator> void swap( concurrent_unordered_multimap<Key, T, Hash, KeyEqual, Allocator>& lhs, concurrent_unordered_multimap<Key, T, Hash, KeyEqual, Allocator>& rhs ) noexcept(noexcept(lhs.swap(rhs)));规范给出的全部语义只有一句话:Equivalent tolhs.swap(rhs),即该非成员函数与成员函数lhs.swap(rhs)完全等价。
从签名可以读出三个关键约束:
- 模板参数完全匹配:
lhs与rhs必须是同一实例化类型(相同的Key、T、Hash、KeyEqual、Allocator)的两个容器,不允许在模板参数不同的两个容器间调用该非成员swap; - 引用传递:两个参数都是左值引用,交换是原地操作,函数不返回值;
- 条件 noexcept:
noexcept(noexcept(lhs.swap(rhs)))是转发式异常规格——非成员函数本身的异常性完全取决于成员swap是否 noexcept,这一点与容器底层实现中分配器的类型直接挂钩(见后文)。
二、仓库中的实际实现:非成员 swap 就是一个转发壳
在仓库内 oneTBB 的头文件 concurrent_unordered_map.h 中,concurrent_unordered_multimap与其非成员swap位于同一文件。非成员swap的实现位于 concurrent_unordered_map.h#L396-L400:
template <typename Key, typename T, typename Hash, typename KeyEqual, typename Allocator> void swap( concurrent_unordered_multimap<Key, T, Hash, KeyEqual, Allocator>& lhs, concurrent_unordered_multimap<Key, T, Hash, KeyEqual, Allocator>& rhs ) { lhs.swap(rhs); }可以看到,实现与规范描述严格一致:函数体只有lhs.swap(rhs)一行,本身不添加任何额外逻辑,因此其异常规格、复杂度、分配器行为全部由成员swap决定。这与同文件中concurrent_unordered_map(单值版本)的非成员swap(concurrent_unordered_map.h#L238-L242)是同一套模式,体现了 oneTBB 对 C++ 标准容器惯用写法(free function 转发到 member function)的遵循。
该函数定义在tbb::detail::d2命名空间内(concurrent_unordered_map.h#L396-L400),并在文件末尾通过inline namespace v1的 using 声明把concurrent_unordered_multimap等类型暴露到tbb::v1(concurrent_unordered_map.h#L405-L411)。因此日常代码中tbb::concurrent_unordered_multimap即可直接使用,非成员swap可通过命名空间限定(tbb::swap(a, b))或 ADL(由于容器类型位于tbb命名空间族内,普通swap(a, b)也能被正确找到)调用。
三、成员 swap 的实现路径:异常安全的常量时间交换
非成员swap只是入口,真正的交换逻辑在容器的基类中。从源码结构看,concurrent_unordered_multimap定义于 concurrent_unordered_map.h#L244-L320,它是concurrent_unordered_base的派生类:
class concurrent_unordered_multimap : public concurrent_unordered_base<concurrent_unordered_map_traits<Key, T, Hash, KeyEqual, Allocator, true>>其中 traits 模板的最后一个布尔参数true标记"允许多值(multimap)"语义。concurrent_unordered_multimap自身并没有重新定义swap,成员swap继承自基类concurrent_unordered_base,实现位于 _concurrent_unordered_base.h#L389-L395:
void swap( concurrent_unordered_base& other ) noexcept(unordered_segment_table::is_noexcept_swap) { if (this != &other) { using pocs_type = typename allocator_traits_type::propagate_on_container_swap; using is_always_equal = typename allocator_traits_type::is_always_equal; internal_swap(other, tbb::detail::disjunction<pocs_type, is_always_equal>()); } }这段实现揭示了三个规范条目中不会展开的细节:
- 自交换保护:
if (this != &other)使swap(a, a)成为安全空操作(no-op),不会对容器自身做任何修改; - noexcept 来自底层段表:
noexcept(unordered_segment_table::is_noexcept_swap)说明异常性由容器底层分段哈希表(segment table)的 swap 是否 noexcept 决定,而这个值最终取决于分配器类型是否保证"所有分配器等价"(见下一节)。这也正是非成员swap采用noexcept(noexcept(lhs.swap(rhs)))转发式规格的原因——它无法在签名层面静态写死,只能跟随成员swap; - 分配器传播决策:在真正交换内部状态(哈希函数、负载因子、各段数据等)时,通过
std::allocator_traits的propagate_on_container_swap(pocs)与is_always_equal两个类型特征做布尔或运算(tbb::detail::disjunction),再分派到internal_swap。
concurrent_unordered_base的 move 赋值实现(_concurrent_unordered_base.h#L370-L381)中可以看到相同的手法——同样基于propagate_on_container_move_assignment与is_always_equal的或运算分派——说明"分配器是否传播"是 oneTBB 所有concurrent_unordered_*容器统一遵循的策略,swap只是其中一环。
四、分配器语义:什么情况下交换会动到 allocator
internal_swap在"需要传播"的路径上会交换两个容器的分配器,该逻辑复用 oneTBB 的通用工具swap_allocators,位于 _allocator_traits.h#L88-L98:
void swap_allocators_impl( Allocator& lhs, Allocator& rhs, /*pocs = */ std::true_type ) { swap(lhs, rhs); } void swap_allocators_impl( Allocator&, Allocator&, /*pocs = */ std::false_type ) {}对应到实际行为,可以归纳为:
| 分配器特征 | 含义 | swap 时分配器的行为 |
|---|---|---|
propagate_on_container_swap == true | 容器 swap 时传播分配器 | 交换内部状态的同时也交换两个分配器实例 |
is_always_equal == true(且 pocs 为 false) | 所有该分配器实例等价 | 交换内部状态,但无需(也不会)交换分配器本身 |
| 两者均为 false | 分配器既不等价也不传播 | 仅交换与分配器无关的内部状态 |
而标准分配器(如std::allocator)与tbb::tbb_allocator的is_always_equal均为true,因此对最常见的默认配置而言,swap可以稳定地是 noexcept 的常量时间操作:只交换哈希函数、比较器、最大负载因子、尺寸/桶计数以及分段表指针等内部成员,不触发任何元素搬移或重新哈希。这也是并发场景下优先用swap而非"拷贝到临时对象再赋值"来整体替换容器内容的原因——它不需要对桶结构做任何逐节点操作。
五、与其他非成员 swap 的对照及实践示例
同一个detail::d2命名空间里,concurrent_unordered_map的非成员swap(concurrent_unordered_map.h#L238-L242)与本文的 multimap 版本完全同构;oneTBB 其他并发容器也遵循同样的"非成员转发成员"惯例,例如concurrent_hash_map(concurrent_hash_map.h#L1657)、concurrent_vector(concurrent_vector.h#L1057)。熟悉其中任意一个即可平移到其余容器。
一个可直接运行的典型用法(交换两个多值映射表的内容,键为字符串、值为任务 ID 列表):
#include <oneapi/tbb/concurrent_unordered_map.h> #include <string> int main() { tbb::concurrent_unordered_multimap<std::string, int> tasks_a, tasks_b; // 多值语义:同一键可对应多个值 tasks_a.emplace("build", 1); tasks_a.emplace("build", 2); tasks_b.emplace("test", 3); // 非成员 swap:等价于 tasks_a.swap(tasks_b) tbb::swap(tasks_a, tasks_b); // 交换后 tasks_a 持有 "test"->3,tasks_b 持有两条 "build" 记录 }使用时需注意两点:其一,两个容器的完整模板参数必须一致(包括分配器),否则不会匹配该非成员重载;其二,若容器中的键/值类型交换成本很高(大型自定义对象),swap依然是首选的整体替换手段,因为如前所述它不搬移任何节点,且默认分配器下是 noexcept 的——在异常安全敏感的并发代码路径中,这一特性比"能用"更重要。
六、小结
- 规范条目 non_member_swap.rst 给出的核心事实是:
concurrent_unordered_multimap的非成员swap与lhs.swap(rhs)等价,异常规格为noexcept(noexcept(lhs.swap(rhs))); - 仓库源码证实该非成员函数是纯转发壳(concurrent_unordered_map.h#L396-L400),真正逻辑位于基类
concurrent_unordered_base::swap(_concurrent_unordered_base.h#L389-L395); - 成员
swap自带自交换保护,并通过propagate_on_container_swap/is_always_equal类型特征决定分配器是否随之交换,默认配置下为常量时间、noexcept 的内部状态交换。
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考