TBB concurrent_unordered_multimap 的构造、析构、拷贝与移动语义:规格说明结合源码逐条解读
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
本篇以 Intel oneAPI TBB(本仓库 vendored 于 third-party/tbb)官方规格文档 construction_destruction_copying.rst 为主体,完整覆盖concurrent_unordered_multimap的空构造、范围/初始化列表构造、拷贝/移动构造、析构与赋值运算的语义约定,并结合 concurrent_unordered_map.h 与 _concurrent_unordered_base.h 的实际实现,讲清每条语义背后的底层机制(分片表复制、强异常安全保证、bucket 数取整、多映射开关等),使读者既能照规格正确使用该容器,也能从源码层面验证其行为边界。
一、容器定位与模板参数:为什么 multimap 只改一个布尔量
concurrent_unordered_multimap是 TBB 提供的线程安全哈希容器,允许同一个 key 对应多个 value(与标准库std::unordered_multimap语义对齐,但支持并发读写)。在本仓库中,其类定义位于 concurrent_unordered_map.h#L244-L320:
template <typename Key, typename T, typename Hash = std::hash<Key>, typename KeyEqual = std::equal_to<Key>, typename Allocator = tbb::tbb_allocator<std::pair<const Key, T>> > class concurrent_unordered_multimap : public concurrent_unordered_base<concurrent_unordered_map_traits<Key, T, Hash, KeyEqual, Allocator, true>>从源码结构看,multimap 与普通concurrent_unordered_map共用同一个 CRTP 基类concurrent_unordered_base(定义于 _concurrent_unordered_base.h#L189),唯一区别是特征结构体的AllowMultimapping模板参数为true(见 concurrent_unordered_map.h#L29-L40 的concurrent_unordered_map_traits)。基类以static constexpr bool allow_multimapping持有该开关(_concurrent_unordered_base.h#L784),在插入路径上直接决定重复 key 的处理:
// _concurrent_unordered_base.h#L1035 if (curr != nullptr && curr->order_key() == order_key && !allow_multimapping) { /* 拒绝重复 key */ } // #L1221 __TBB_ASSERT(!allow_multimapping, "Insertion should succeed for multicontainer");即 multimap 下插入同 key 的元素总是成功,这一行为从构造起就被模板参数固化。
规格文档中所有构造函数、析构函数与赋值运算符的最终实现都落在基类concurrent_unordered_base,multimap 类仅通过 concurrent_unordered_map.h#L271 的using base_type::base_type;将基类构造器引入,并按“rule of 5”把拷贝/移动构造与赋值声明为= default(concurrent_unordered_map.h#L275-L282),再转发给基类。下面按规格文档的原始顺序逐节展开。
二、空容器构造函数
规格文档定义了 4 个空容器构造重载:
concurrent_unordered_multimap(); explicit concurrent_unordered_multimap( const allocator_type& alloc ); explicit concurrent_unordered_multimap( size_type bucket_count, const hasher& hash = hasher(), const key_equal& equal = key_equal(), const allocator_type& alloc = allocator_type() ); concurrent_unordered_multimap( size_type bucket_count, const allocator_type& alloc ); concurrent_unordered_multimap( size_type bucket_count, const hasher& hash, const allocator_type& alloc );语义要点(继承自规格文档):
- 全部构造出一个空容器;不指定
bucket_count时,初始桶数“未指定”(unspecified); - 提供
hasher/key_equal/allocator参数时,分别用于哈希计算、key 相等性判断与内存分配。
源码对“初始桶数未指定”给出了精确答案。默认构造在基类中委托给initial_bucket_count(_concurrent_unordered_base.h#L253),该常量在 第 787-788 行 定义为:
static constexpr size_type initial_bucket_count = 8; static constexpr float initial_max_load_factor = 4; // TODO: consider 1?而带bucket_count的构造会把它向上取整为 2 的幂(第 244-251 行):
explicit concurrent_unordered_base( size_type bucket_count, const hasher& hash = hasher(), const key_equal& equal = key_equal(), const allocator_type& alloc = allocator_type() ) : my_size(0), my_bucket_count(round_up_to_power_of_two(bucket_count)), my_max_load_factor(float(initial_max_load_factor)), my_hash_compare(hash, equal), my_head(sokey_type(0)), my_segments(alloc) {}可以推断,取 2 的幂是为了让“哈希值取模分桶”可以退化为位与运算,加速并发插入/查找中的分桶定位。初始最大装载因子固定为 4.0,源码中的 TODO 注释表明作者曾考虑过收紧到 1,当前版本以 4 为默认值。
三、来自元素序列的构造函数(范围构造)
规格文档给出三组迭代器构造重载,要求InputIterator满足 ISO C++ 标准 [input.iterators] 节的要求:
template <typename InputIterator> concurrent_unordered_multimap( InputIterator first, InputIterator last, size_type bucket_count = /*implementation-defined*/, const hasher& hash = hasher(), const key_equal& equal = key_equal(), const allocator_type& alloc = allocator_type() ); template <typename InputIterator> concurrent_unordered_multimap( InputIterator first, InputIterator last, size_type bucket_count, const allocator_type& alloc ); template <typename InputIterator> concurrent_unordered_multimap( InputIterator first, InputIterator last, size_type bucket_count, const hasher& hash, const allocator_type& alloc );容器构造完成后包含半开区间[first, last)的全部元素。基类实现印证了这一语义(第 264-281 行):先以完整参数列表完成桶表初始化,再对区间调用insert(first, last)逐个插入:
template <typename InputIterator> concurrent_unordered_base( InputIterator first, InputIterator last, size_type bucket_count = initial_bucket_count, const hasher& hash = hasher(), const key_equal& equal = key_equal(), const allocator_type& alloc = allocator_type() ) : concurrent_unordered_base(bucket_count, hash, equal, alloc) { insert(first, last); }注意默认bucket_count即“implementation-defined”的具体值——就是前述的initial_bucket_count(8)。
四、初始化列表构造函数
规格文档列出了std::initializer_list<value_type>的三个重载,并逐条声明了等价关系:
concurrent_unordered_multimap( std::initializer_list<value_type> init, size_type bucket_count = /*implementation-defined*/, const hasher& hash = hasher(), const key_equal& equal = key_equal(), const allocator_type& alloc = allocator_type() ); // 等价于 concurrent_unordered_multimap(init.begin(), init.end(), bucket_count, hash, equal, alloc) concurrent_unordered_multimap( std::initializer_list<value_type> init, size_type bucket_count, const allocator_type& alloc ); // 等价于 concurrent_unordered_multimap(init.begin(), init.end(), bucket_count, alloc) concurrent_unordered_multimap( std::initializer_list<value_type> init, size_type bucket_count, const hasher& hash, const allocator_type& alloc ); // 等价于 concurrent_unordered_multimap(init.begin(), init.end(), bucket_count, hash, alloc)源码与规格严格一致,均为转发到对应的迭代器版本(第 336-348 行):
concurrent_unordered_base( std::initializer_list<value_type> init, size_type bucket_count = initial_bucket_count, const hasher& hash = hasher(), const key_equal& equal = key_equal(), const allocator_type& alloc = allocator_type() ) : concurrent_unordered_base(init.begin(), init.end(), bucket_count, hash, equal, alloc) {}配合 C++17 类模板参数推导(CTAD),用户甚至无需写出模板实参——仓库中 concurrent_unordered_map.h#L322-L394 为 multimap 提供了一整套显式推导指南(从迭代器或initializer_list推导Key/T/Hash/KeyEqual/Allocator),规格侧的完整说明见同目录的 deduction_guides.rst。例如:
std::vector<std::pair<int, float>> v; oneapi::tbb::concurrent_unordered_multimap m1(v.begin(), v.end()); // 推导为 <int, float> oneapi::tbb::concurrent_unordered_multimap m2(v.begin(), v.end(), CustomHasher{}); // 推导 Hash=CustomHasher oneapi::tbb::concurrent_unordered_multimap m3 = { {1, 1.0f}, {2, 2.0f} }; // 初始化列表 + CTAD五、拷贝构造函数
规格文档定义:
concurrent_unordered_multimap( const concurrent_unordered_multimap& other ); concurrent_unordered_multimap( const concurrent_unordered_multimap& other, const allocator_type& alloc );三条语义约定必须完整掌握:
- 构造出
other的一份深拷贝; - 若未显式提供分配器,则通过
std::allocator_traits<allocator_type>::select_on_container_copy_construction(other.get_allocator())取得——这正是 C++ 标准对 allocator-aware 容器拷贝构造的统一要求; - 若拷贝过程中存在与
other的并发操作,行为未定义(UB)。并发容器保证的是“多方线程同时操作同一容器”,而不是“在读取other的同时另一个线程正在修改other”。
基类实现(第 283-311 行)展示了拷贝的具体步骤与异常安全策略:
concurrent_unordered_base( const concurrent_unordered_base& other ) : my_size(other.my_size.load(std::memory_order_relaxed)), my_bucket_count(other.my_bucket_count.load(std::memory_order_relaxed)), my_max_load_factor(other.my_max_load_factor), my_hash_compare(other.my_hash_compare), my_head(other.my_head.order_key()), my_segments(other.my_segments) // 先拷贝分片元数据表 { try_call( [&] { internal_copy(other); // 再深拷贝各分片中的节点 } ).on_exception( [&] { clear(); // 节点复制抛异常时清理半成品 }); }从源码结构看:分片元数据表my_segments(unordered_segment_table)先被整体拷贝,节点内容随后经internal_copy逐分片、按 order-key 有序地重建;try_call+on_exception(clear)的组合提供了“要么成功、要么不留痕迹”的强异常保证——如果某个元素拷贝构造抛出异常,半成品容器会被清空析构,不会泄漏节点。带alloc参数的重载逻辑相同,只是my_segments改用指定分配器构造(第 298-311 行)。
六、移动构造函数
规格文档定义:
concurrent_unordered_multimap( concurrent_unordered_multimap&& other ); concurrent_unordered_multimap( concurrent_unordered_multimap&& other, const allocator_type& alloc );- 以移动语义接管
other的内容;other被置于“有效但未指定”的状态; - 未提供分配器时,分配器取自
std::move(other.get_allocator())(规格原文如此表述,对应 C++ 标准移动语义惯例); - 同样,与
other并发操作时行为未定义。
基类移动构造(第 313-334 行)与拷贝构造形成鲜明对比——没有任何节点级复制:
concurrent_unordered_base( concurrent_unordered_base&& other ) : my_size(other.my_size.load(std::memory_order_relaxed)), my_bucket_count(other.my_bucket_count.load(std::memory_order_relaxed)), my_max_load_factor(std::move(other.my_max_load_factor)), my_hash_compare(std::move(other.my_hash_compare)), my_head(other.my_head.order_key()), my_segments(std::move(other.my_segments)) // 分片表整体搬走 { move_content(std::move(other)); // 把节点链接重接到新表上 }带分配器版本会额外检查allocator_traits_type::is_always_equal,调用internal_move_construct_with_allocator:当两个分配器必然相等(如标准std::allocator)时节点可零成本搬运,否则需要逐节点用新分配器重建。这与 C++ 标准容器对 allocator 可传播性的要求一致。
七、析构函数
~concurrent_unordered_multimap();规格约定:销毁容器、调用所有存储元素的析构函数、释放所占存储;与*this并发操作时行为未定义。
实现一行即可见全貌(第 350-352 行):
~concurrent_unordered_base() { internal_clear(); }internal_clear()遍历各分片释放全部节点并回收分片表,同时会复位my_size与my_bucket_count(第 1368-1369 行 附近将桶数恢复为initial_bucket_count),这也解释了移动后other处于“有效但未指定”状态的具体成因之一。
八、赋值运算符
8.1 拷贝赋值
concurrent_unordered_multimap& operator=( const concurrent_unordered_multimap& other );- 用
other中元素的拷贝替换*this的全部元素,返回*this引用; - 当
std::allocator_traits<allocator_type>::propagate_on_container_copy_assignment::value为true时才复制分配器; - 与
*this、other任一发生并发操作时行为未定义。
基类实现(第 354-365 行)是典型的“清空再重建”策略,并显式做了自赋值保护:
concurrent_unordered_base& operator=( const concurrent_unordered_base& other ) { if (this != &other) { clear(); my_size.store(other.my_size.load(std::memory_order_relaxed), std::memory_order_relaxed); my_bucket_count.store(other.my_bucket_count.load(std::memory_order_relaxed), std::memory_order_relaxed); my_max_load_factor = other.my_max_load_factor; my_hash_compare = other.my_hash_compare; my_segments = other.my_segments; internal_copy(other); } return *this; }8.2 移动赋值及其 noexcept 规格
concurrent_unordered_multimap& operator=( concurrent_unordered_multimap&& other ) noexcept(/*See below*/);- 以移动语义替换全部元素,
other置于有效但未指定状态,返回*this引用; - 当
propagate_on_container_move_assignment::value为true时移动分配器; noexcept表达式(规格原文):
noexcept(std::allocator_traits<allocator_type>::is_always_equal::value && std::is_nothrow_move_assignable<hasher>::value && std::is_nothrow_move_assignable<key_equal>::value)源码中的实现(第 367 行起)在结构上一致:clear()后用std::move接管my_max_load_factor、my_hash_compare、my_segments。其noexcept判定由unordered_segment_table::is_noexcept_assignment给出——该值正是由分配器、hasher、key_equal三者的可无抛移动性共同决定,与规格表达式对应。实践中,默认的tbb::tbb_allocator、std::hash、std::equal_to组合下移动赋值实际是无抛的,可安全用于“先构建、后换入”的容器更新模式。
8.3 初始化列表赋值
concurrent_unordered_multimap& operator=( std::initializer_list<value_type> init );- 用
init中的元素替换*this的全部元素,返回*this引用; - 若
init中存在多个 key 相等的元素,插入哪个是未指定的——注意对 multimap 而言这一条实际影响很小(重复 key 本身允许共存,差别仅在于内部 order-key 排序导致的存储顺序),规格仍按统一模板保留了该声明; - 与
*this并发操作时行为未定义。
multimap 侧的实现就是两行的转发(concurrent_unordered_map.h#L284-L287):
concurrent_unordered_multimap& operator=( std::initializer_list<value_type> il ) { base_type::operator= (il); return *this; }九、使用要点小结
把规格与源码对照后,日常使用该容器应把握以下要点:
- 桶数可预测:传
bucket_count时会被向上取整到 2 的幂且不产生更多分片成本;不传则默认 8 桶、装载因子 4.0。对已知数据规模,显式传入桶数可减少扩容时的分桶重定位(扩容逻辑见 第 674 行 的compare_exchange_strong路径)。 - 拷贝是深拷贝且强异常安全,但要求拷贝期间无人并发修改源容器;把“并发”理解为跨线程同时读写同一个容器是安全,而非两个容器间的任意交叉操作都安全。
- 移动构造/移动赋值是 O(1) 级元数据接管(默认分配器场景),是跨线程移交容器所有权(如 producer 线程构建完成后 move 给 consumer)的推荐方式。
- 重复 key 永远可插入:由
allow_multimapping = true在插入路径上保证(第 1035 行、第 1221 行),这是它与concurrent_unordered_map的本质区别。 - 配套接口(观察、查找、bucket、并行迭代、
swap等)分别在规格目录 concurrent_unordered_multimap_cls 下的 observers.rst、lookup.rst、non_member_swap.rst 等文档中继续展开;头文件层面还可用split将容器按桶拆分、再用merge合并(concurrent_unordered_map.h#L301-L319 的merge重载)。
本文全部结论均来自本仓库内 TBB 规格文档(third-party/tbb/doc/main/specification/source/containers/concurrent_unordered_multimap_cls/construction_destruction_copying.rst)与实现头文件(third-party/tbb/include/oneapi/tbb/concurrent_unordered_map.h、third-party/tbb/include/oneapi/tbb/detail/_concurrent_unordered_base.h),适用于该 vendored 版本(Copyright 标注 2005-2024 的 oneTBB 源码);其他 TBB 版本中常量取值(如initial_bucket_count)可能不同,请以对应版本源码为准。
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考