oneAPI TBB concurrent_multiset 并行迭代完全指南:range_type、const_range_type 与 range() 实战解析

发布时间:2026/9/14 17:48:04
oneAPI TBB concurrent_multiset 并行迭代完全指南:range_type、const_range_type 与 range() 实战解析 oneAPI TBB concurrent_multiset 并行迭代完全指南range_type、const_range_type 与 range() 实战解析【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold导读oneapi::tbb::concurrent_multiset是 oneAPI Threading Building BlocksTBB提供的并发有序多重集合容器支持并发插入、查找与遍历。本指南围绕其并行迭代机制展开详解range_type/const_range_type两种范围类型的设计差异、range()成员函数的语义并结合 TBB 的ContainerRange与Range规范需求从源码层面剖析范围对象的begin/end/grainsize/is_divisible/ 分裂构造器splitting constructor等关键接口最终给出基于parallel_for的真实并行遍历方案。读完本文你将掌握如何在多线程场景下安全、高效地并行扫描一个concurrent_multiset并理解 TBB 范围-分治range-splitting模型在该容器上的具体落地方式。一、并行迭代在 concurrent_multiset 中的定位在深入并行迭代之前需要明确concurrent_multiset的整体并发模型。根据容器类规范见 concurrent_multiset_cls.rst它是一个表示有序元素序列的类模板支持并发插入、查找和遍历concurrently safe insertion, lookup, and traversal不支持并发删除——删除操作必须显式使用unsafe_erase/unsafe_extract等并发不安全修饰符允许存储多个等价元素与concurrent_set的关键区别。在源码层面concurrent_multiset声明于头文件 concurrent_set.h// 位于 third-party/tbb/include/oneapi/tbb/concurrent_set.h template typename Key, typename Compare std::lessKey, typename Allocator tbb::tbb_allocatorKey class concurrent_multiset : public concurrent_skip_listset_traitsKey, Compare, geometric_level_generator32, Allocator, true { ... // Parallel iteration range_type range(); const_range_type range() const; };从源码结构可以看出concurrent_multiset直接继承自 TBB 内部的concurrent_skip_list并发跳表并以set_traits中的allow_multimapping true开启多值映射并行迭代所需的range()与range_type/const_range_type均由跳表基类统一提供详见 _concurrent_skip_list.h。二、range_type 与 const_range_type两种范围类型的唯一区别关联文档 parallel_iteration.rst 首先定义了两种成员类型concurrent_multiset::range_typeconcurrent_multiset::const_range_type二者都满足ContainerRange需求详见下文第三节并且唯一的区别在于边界迭代器的类型范围类型边界迭代器类型用途range_typeconcurrent_multiset::iterator非 const 容器上调用range()得到可写遍历const_range_typeconcurrent_multiset::const_iteratorconst 容器或 const 引用上调用range()得到只读遍历这一设计在源码中有直接对应。在 _concurrent_skip_list.h 中基类先定义了const_range_typeclass const_range_type { public: using size_type typename concurrent_skip_list::size_type; using difference_type typename concurrent_skip_list::difference_type; using iterator typename concurrent_skip_list::const_iterator; // 注意const 迭代器 using value_type typename iterator::value_type; using reference typename iterator::reference; bool empty() const { ... } bool is_divisible() const { ... } size_type size() const { return std::distance(my_begin, my_end); } const_range_type( const_range_type r, split ); // 分裂构造器 const_range_type( const concurrent_skip_list l ); iterator begin() const { return my_begin; } iterator end() const { return my_end; } size_type grainsize() const { return 1; } ... };随后range_type公开继承const_range_type仅将边界迭代器替换为可写的concurrent_skip_list::iterator并覆写begin()/end()见 _concurrent_skip_list.hclass range_type : public const_range_type { public: using iterator typename concurrent_skip_list::iterator; using value_type typename iterator::value_type; using reference typename iterator::reference; range_type(range_type r, split) : const_range_type(r, split()) {} range_type(const concurrent_skip_list l) : const_range_type(l) {} iterator begin() const { return iterator(const_range_type::begin().my_node_ptr); } iterator end() const { return iterator(const_range_type::end().my_node_ptr); } };可以推断range_type采用覆写迭代器类型而非复制逻辑的方式实现因此两种范围类型在empty、is_divisible、size、grainsize等行为上完全一致差异仅体现在返回的迭代器可否解引用为可变引用上。三、range() 成员函数一次性获得整容器范围range()是进入并行迭代的入口规范签名如下range_type range(); const_range_type range() const;Returns: 一个表示容器中所有元素的范围对象a range object representing all elements in the container。在非 const的concurrent_multiset对象上调用返回range_type迭代器可写在const对象或 const 引用上调用返回const_range_type迭代器只读。源码实现见 _concurrent_skip_list.hrange_type range() { return range_type(*this); } const_range_type range() const { return const_range_type(*this); }两个重载分别以*this构造对应范围对象。从const_range_type的构造函数第 743-745 行可以看到范围对象内部保存的是容器的begin()与end()迭代器并记录起始节点高度my_levelconst_range_type( const concurrent_skip_list l) : my_end(l.end()), my_begin(l.begin()), my_level(my_begin.my_node_ptr ? my_begin.my_node_ptr-height() : 0) {}注意range()返回的范围是容器在调用时刻的视图快照迭代器对。由于concurrent_multiset支持并发插入遍历过程中其他线程的插入会反映到跳表结构中而范围对象仅持有首尾迭代器因此它表达的是从当前首元素到当前尾元素的一段遍历窗口。四、ContainerRange 需求范围类型必须满足的接口契约range_type与const_range_type之所以能直接喂给parallel_for等并行算法是因为它们满足 TBB 规范中的ContainerRange需求。完整契约见 container_range.rst标记[req.container_range]ContainerRange是表示一个并发容器或其一部分的范围可用于parallel_for等并行算法中的并行遍历。一个类型CR满足ContainerRange需求需要同时满足Range需求详见第五节并提供以下成员类型与函数成员类型 / 签名语义CR::value_type类型范围内元素的类型CR::reference类型范围内元素的引用类型CR::const_reference类型范围内元素的常量引用类型CR::iterator类型遍历范围所用的迭代器类型CR::size_type无符号整型用于获取粒度grain sizeCR::difference_type有符号整型两个迭代器之差的结果类型CR::begin()iterator CR::begin()返回指向范围首元素的迭代器CR::end()iterator CR::end()返回指向范围尾元素之后位置的迭代器CR::grainsize() constsize_type返回范围的粒度grain size对照第二节的源码const_range_type恰好逐一实现了上述契约value_type/reference取自迭代器begin()/end()返回首尾迭代器grainsize()固定返回1。这意味着每个叶子子范围在parallel_for眼中代表至少一个元素的串行工作块。五、Range 需求parallel_for 如何切分并调度范围ContainerRange之上还叠加了更基础的Range需求定义于 range.rst。并行算法正是通过以下接口递归拆分范围、实现负载均衡的bool R::empty() const; // 范围是否为空 bool R::is_divisible() const; // 范围是否可继续拆分 R::R( R r, split ); // 基础分裂构造器把 r 拆成两个子范围 R::R( R r, proportional_split proportion ); // 可选按比例拆分对应_concurrent_skip_list.h中的实现bool empty() const { return my_begin.my_node_ptr ? (my_begin.my_node_ptr-next(0) my_end.my_node_ptr) : true; } bool is_divisible() const { return my_begin.my_node_ptr my_level ! 0 ? my_begin.my_node_ptr-next(my_level - 1) ! my_end.my_node_ptr : false; } size_type size() const { return std::distance(my_begin, my_end); }从实现可以读出跳表范围拆分的核心思路empty()起始节点为空或首节点第 0 层的后继就是尾节点视为空范围is_divisible()判断起始节点在my_level - 1层上的后继是否越过尾节点只有当前范围跨越了更高层的快速通道才值得继续拆分分裂构造器第 730-741 行把原范围从my_level - 1层的后继处拦腰截断前一半留给原对象后一半构造为新对象新对象的my_level取切分节点的高度。这实际上是利用跳表的层高信息实现近似等分的 O(1) 拆分避免了线性扫描。const_range_type( const_range_type r, split) : my_end(r.my_end) { if (r.empty()) { my_begin my_end; my_level 0; } else { my_begin iterator(r.my_begin.my_node_ptr-next(r.my_level - 1)); my_level my_begin.my_node_ptr-height(); } r.my_end my_begin; // 原范围截断为前半段 }split标签类型定义于 TBB 内部头文件_range_common.hclass split {};它只是一个用于区分分裂构造器与拷贝构造器的标记类型TBB 的并行算法通过range_split_object_provider等机制向范围对象传递split或proportional_split见_range_common.h中的get_range_split_object与tbb_range概念约束copy_constructible splittable is_divisible()。六、实战用 parallel_for 并行遍历 concurrent_multiset基于以上接口标准用法是把range()的返回值直接交给parallel_for再在 lambda 中消费子范围#include oneapi/tbb/concurrent_set.h #include oneapi/tbb/parallel_for.h #include oneapi/tbb/blocked_range.h #include string #include vector int main() { oneapi::tbb::concurrent_multisetstd::string words; // 并发阶段多线程向容器插入数据插入是并发安全的 // ... words.emplace(apple); words.emplace(banana); ... // 并行遍历一次性获取整容器范围交由 parallel_for 自动拆分调度 std::vectorstd::size_t per_word_lengths; oneapi::tbb::parallel_for(words.range(), { // 每个工作线程处理一个子范围递归拆分后的叶子块 for (auto it sub_range.begin(); it ! sub_range.end(); it) { // 只读访问元素若使用非 const 容器 非 const range() // 这里还可以安全地修改元素修改需自行保证对 key 比较不产生冲突 } }); return 0; }关键点类型推导words.range()在非 const 容器上返回range_type其begin()/end()是可写的iteratorlambda 参数若写成const auto实际绑定到的还是同一个范围对象粒度控制范围对象的grainsize()固定为1源码第 749 行即最小串行块为一个元素parallel_for内部还会结合其分区器partitioner的自动粒度调整来决定实际拆分深度通常无需手动干预const 场景若持有的是const concurrent_multiset或const auto则range()返回const_range_type遍历只读这是并发场景下的推荐路径。七、设计要点与注意事项7.1 为什么 range_type 继承 const_range_type从 _concurrent_skip_list.h 的实现可以推断这种const 版本为基类、可变版本为派生类的设计让范围拆分逻辑分裂构造器、my_level维护、is_divisible只需实现一次range_type通过继承复用全部拆分行为仅替换迭代器类型既避免了代码重复也保证了两种范围在并行算法中的行为完全一致。7.2 遍历与并发修改的相互作用concurrent_multiset的插入是并发安全的因此遍历期间其他线程插入新元素不会破坏容器结构跳表节点一经发布便不可变迭代器解引用始终安全。删除不在并发安全之列erase系列必须使用unsafe_前缀的方法unsafe_erase、unsafe_extract这些方法不得与遍历、查找等操作并发执行否则属于数据竞争见容器类规范的 Concurrently unsafe modifiers 部分concurrent_multiset_cls.rst。若确有并发删除需求应改用支持并发删除的容器族如concurrent_hash_map、concurrent_unordered_multiset。7.3 遍历结果的有序性concurrent_multiset底层是并发跳表元素按Compare默认std::lessT保持有序因此range()产生的范围遍历天然按序输出即使范围被切分为多个子范围并行处理每个子范围内部依然保持相对有序。7.4 多值语义由于allow_multimapping trueconcurrent_multiset可以容纳多个等价元素并行遍历时等价元素会分布在相邻位置若需要在遍历时统计每个值的出现次数sub_range内部的相邻相等元素需要自行合并处理例如按*it分组统计。八、扩展阅读路径并行迭代规范原文parallel_iteration.rst容器类整体规范成员函数索引、非成员比较/交换、推导指南concurrent_multiset_cls.rstContainerRange需求完整契约container_range.rstRange需求与分裂构造器约定range.rst迭代器规范ForwardIterator需求iterators.rst源码实现范围类型与range()定义_concurrent_skip_list.h容器类声明与模板参数concurrent_set.h同类容器的并行迭代文档可对照阅读concurrent_set 并行迭代、concurrent_multimap 并行迭代结语concurrent_multiset的并行迭代能力由一对精心设计的范围类型承载const_range_type提供完整的ContainerRange/Range契约实现range_type在只读基础上叠加可写迭代器range()作为统一入口把整容器快照交给parallel_for递归拆分。理解了跳表层高驱动的 O(1) 分裂构造器与grainsize() 1的粒度约定你就能准确预判并行遍历的切分行为写出既安全又高效的并发扫描代码——这正是 TBB 容器族并发安全遍历能力的精髓所在。【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考