mold 仓库内嵌 oneTBB 的 concurrent_map 并行迭代指南:range() 与 ContainerRange 需求深入解析
mold 仓库内嵌 oneTBB 的 concurrent_map 并行迭代指南range() 与 ContainerRange 需求深入解析【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold导读本文聚焦于 mold 仓库 third-party 目录下内嵌的 oneTBBoneAPI Threading Building Blocks中oneapi::tbb::concurrent_map的并行迭代机制围绕官方规范文档 parallel_iteration.rst 展开系统讲解range_type/const_range_type两个成员类型与range()成员函数的语义并结合ContainerRange/Range需求规范与 _concurrent_skip_list.h 源码实现给出可直接用于parallel_for的实战示例。读完本文你将掌握如何把并发容器安全、高效地接入 oneTBB 并行算法框架理解其底层跳表切分原理并能在自己的并发编程场景中正确使用该接口。一、背景concurrent_map 是什么oneapi::tbb::concurrent_map是 oneTBB 提供的一个类模板表示有序关联容器sorted associative container。根据官方类规范 concurrent_map_cls.rst 的描述存储唯一键元素unique elements支持并发的插入、查找与遍历concurrent insertion, lookup, and traversal不支持并发的删除操作——所有擦除接口都以unsafe_前缀命名如unsafe_erase意味着擦除必须由调用方自行保证与其它操作互斥。其类模板声明为namespace oneapi { namespace tbb { template typename Key, typename T, typename Compare std::lessKey, typename Allocator tbb_allocatorstd::pairconst Key, T class concurrent_map { public: using key_type Key; using mapped_type T; using value_type std::pairconst Key, T; // ... using iterator implementation-defined ForwardIterator; using const_iterator implementation-defined constant ForwardIterator; using range_type implementation-defined range; using const_range_type implementation-defined constant range; // ... // Parallel iteration range_type range(); const_range_type range() const; }; // class concurrent_map } // namespace tbb } // namespace oneapi从实现上看concurrent_map并非自建容器而是继承自跳表skip list基类。见 concurrent_map.hclass concurrent_multimap; class concurrent_map : public concurrent_skip_listmap_traitsKey, Value, Compare, geometric_level_generator32, Allocator, false {也就是说concurrent_map的并发性、有序性以及并行迭代能力最终都由底层的concurrent_skip_list提供这也是后文分析range_type实现时要回到_concurrent_skip_list.h的原因。二、并行迭代的入口range_type 与 const_range_type 成员类型并行迭代的核心思想是把整个容器表示为一个可递归切分的区间对象range交给parallel_for/parallel_reduce等并行算法去分割调度而不是简单地用单个迭代器在多个线程间手工切分。concurrent_map为此提供了两个成员类型成员类型边界迭代器类型用途concurrent_map::range_typeconcurrent_map::iterator非 const 版本遍历时可修改元素的 mapped valueconcurrent_map::const_range_typeconcurrent_map::const_iteratorconst 版本遍历时只能读取元素规范原文明确指出这两个类型唯一区别在于边界bounds的迭代器类型const_range_type的边界是const_iterator而range_type的边界是iterator。因此对非 const 的concurrent_map对象调用range()返回range_type遍历过程中得到的是可写引用value_type其中value_type std::pairconst Key, T键不可改、映射值可改对 const 对象调用range()返回const_range_type遍历过程中只能读取元素。两个类型都满足 oneTBB 的ContainerRange 需求见下文第三节因而可以被并行算法直接消费。三、range() 成员函数签名与语义规范的 parallel_iteration.rst 给出range()的完整签名range_type range(); const_range_type range() const;返回值一个表示容器中全部元素all elements in the container的 range 对象。调用方式非常直观oneapi::tbb::concurrent_mapint, int m; // ... 填充数据 ... auto r m.range(); // range_type非 const 对象 const auto cm m; auto cr cm.range(); // const_range_typeconst 对象从源码实现看这两个重载只是简单地用容器自身构造 range 对象见 _concurrent_skip_list.hrange_type range() { return range_type(*this); } const_range_type range() const { return const_range_type(*this); }其中range_type/const_range_type的构造会记录容器的begin()、end()以及起始节点高度跳表层级见 _concurrent_skip_list.hconst_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()返回的 range 对象表示的是调用时刻容器中的元素。在后续并行遍历期间若其它线程并发插入新元素遍历是否能看到这些新元素由底层跳表实现决定跳表遍历沿链表前进通常只能看到已链接入链的节点这是并发容器的固有语义与begin()/end()迭代器的行为一致。四、ContainerRange 需求range 对象必须提供什么range_type与const_range_type声称满足 ContainerRange 需求这是它们能被parallel_for等算法接受的前提。ContainerRange 需求定义在 container_range.rstContainerRange是一个表示并发容器或其一部分的 range。该 range 对象可用于在parallel_for等并行算法中遍历容器。一个类型CR满足 ContainerRange 需求需要满足Range 需求见下提供以下成员类型CR::value_type—— range 中元素的类型CR::reference—— 元素的引用类型CR::const_reference—— 元素的常引用类型CR::iterator—— 用于遍历 range 的迭代器类型CR::size_type—— 用于获取 grain size 的无符号整数类型CR::difference_type—— 两个迭代器差值的类型提供以下成员函数iterator CR::begin()—— 返回 range 起始迭代器iterator CR::end()—— 返回 range 末尾之后位置的迭代器size_type CR::grainsize() const—— 返回 range 的粒度grain size。对照 _concurrent_skip_list.h 中const_range_type的实现可以逐项印证class 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; 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; } // ... };从源码结构可以看到该实现中grainsize()固定返回1即每个元素都被视为不可再分割的最小工作单元切分粒度以元素为单位。五、Range 需求递归切分的底层契约ContainerRange 需求建立在更基础的Range 需求之上其定义见 range.rst。Range 需求的核心是一个可递归二分的区间抽象R::R( const R )—— 拷贝构造bool R::empty() const—— 区间是否为空bool R::is_divisible() const—— 区间是否还能分成两个子区间R::R( R r, split )——基本切分构造函数splitting constructor把r一分为二建议尽量均分但非强制越均匀通常并行度越好R::R( R r, proportional_split proportion )——可选的比例切分构造函数按比例分割。正是借助split/proportional_split这两个哨兵类型C 才能在拷贝构造与切分构造之间做出区分。相关标签类型定义在 _range_common.h//! Dummy type that distinguishes splitting constructor from copy constructor. class split {}; //! Type enables transmission of splitting proportion from partitioners to range objects class proportional_split : no_assign { public: proportional_split(size_t _left 1, size_t _right 1) : my_left(_left), my_right(_right) { } // used when range does not support proportional split explicit operator split() const { return split(); } };Range 需求还强调一个约定切分构造应构造第二部分并把参数更新为第一部分。这样当parallel_for、parallel_reduce、parallel_scan串行执行时会按递增顺序处理区间与普通循环行为一致。由于 range 类型声明了切分构造与拷贝构造编译器不会自动生成默认构造函数需要显式提供其它构造函数才能创建实例——concurrent_map的 range 正是通过接收容器的构造函数来满足这一点的。六、源码级剖析跳表 range 是如何切分的理解了需求契约再看concurrent_map的 range 如何在跳表上实现切分就一目了然。6.1 range_type 与 const_range_type 的关系_concurrent_skip_list.h 中range_type直接公有继承自const_range_typeclass 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 { node_ptr node const_range_type::begin().my_node_ptr; return iterator(node); } iterator end() const { node_ptr node const_range_type::end().my_node_ptr; return iterator(node); } }; // class range_type这与规范两类型仅在边界迭代器类型上不同的描述完全吻合range_type复用父类的切分逻辑只把begin()/end()的返回类型从const_iterator换成iterator从而允许在遍历中修改mapped value。6.2 基本切分构造按跳表层高二分const_range_type的切分构造是并行化的关键见 _concurrent_skip_list.hconst_range_type( const_range_type r, split) : my_end(r.my_end) { if (r.empty()) { __TBB_ASSERT(my_end.my_node_ptr nullptr, nullptr); 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; }其切分策略是取当前区间起始节点在第my_level - 1层即当前记录的最高层的后继节点作为新子区间的起点把原区间的my_end更新为这个新起点——即后半段从跳表高层直接跳到一个中位节点开始。这正是利用了跳表的多层索引结构实现近似均分同时my_level会随子区间起点重新取height()以适配后续继续切分。6.3 empty 与 is_divisibleempty()my_begin为nullptr或my_begin-next(0)第 0 层后继等于my_end时为空见 _concurrent_skip_list.his_divisible()仅当起始节点存在、my_level ! 0且第my_level - 1层的后继不等于my_end时才可继续切分见 _concurrent_skip_list.h。这两个谓词决定了并行算法何时停止递归切分当子区间退化到只剩单个元素或不足一层跨度时is_divisible()返回false该子区间便交给单个线程串行处理。七、实战将 range() 接入 parallel_for按照 ContainerRange 需求的设计意图range()的典型用法是直接作为parallel_for的区间参数。示例代码如下#include oneapi/tbb/concurrent_map.h #include oneapi/tbb/parallel_for.h #include cstdio int main() { // 构造并填充并发有序表键 0..999值为平方 oneapi::tbb::concurrent_mapint, int squares; for (int i 0; i 1000; i) squares.emplace(i, i * i); // 并行遍历对每个元素求和 long long sum 0; oneapi::tbb::parallel_for(squares.range(), { long long local 0; for (auto it r.begin(); it ! r.end(); it) { local it-second; // it-first 为键it-second 为映射值 } // 合并局部结果示例用原子操作简化生产代码可改用 parallel_reduce __atomic_fetch_add(sum, local, __ATOMIC_RELAXED); }); std::printf(sum %lld\n, sum); return 0; }执行流程squares.range()生成覆盖全部元素的range_type对象内部记录begin、end与起始节点高度parallel_for反复调用切分构造把区间二分直到is_divisible()为false每个工作线程拿到一个不可再分的子区间串行遍历r.begin(), r.end())由于 range 内部使用跳表第 0 层链表串联遍历时天然保持键的升序concurrent_map是有序容器。若只需要只读遍历可对 const 对象调用range()得到const_range_type避免误改数据const auto c squares; oneapi::tbb::parallel_for(c.range(), [ { for (auto it r.begin(); it ! r.end(); it) { std::printf(%d - %d\n, it-first, it-second); } });八、并发语义与使用注意事项基于规范与源码使用concurrent_map并行迭代时有几点需要明确遍历与插入可以并发concurrent_map支持并发插入、查找与遍历因此parallel_for遍历期间其他线程继续insert/emplace是安全的这是跳表无锁实现的基本保证。擦除必须独占所有删除接口均为unsafe_*unsafe_erase、unsafe_extract、clear与并行遍历/插入并发调用属于未定义行为。若确有删除需求需在外部加锁或改用其它方案。迭代器满足 ForwardIteratorconcurrent_map::iterator/const_iterator满足 C 标准 [forward.iterators] 段的前向迭代器需求见 iterators.rst因此可以被 range 正常持有与递增。修改的是映射值而非键value_type为std::pairconst Key, T键不可变通过range_type的迭代器可以修改mapped value这不会破坏有序性。grainsize 为 1从源码可见当前实现的grainsize()固定返回1即元素粒度不可再分若遍历体开销极小而元素极多可自行在遍历体内做批量聚合以摊薄调度开销。九、同一模式的其它容器通过range()获得 ContainerRange 并接入并行算法是 oneTBB 并发容器族的统一模式。在 containers 规范目录 下concurrent_hash_map、concurrent_set、concurrent_multimap、concurrent_unordered_map、concurrent_vector等均提供对应的 parallel_iteration.rst 页面语义与本文所述一致仅 range 内部切分机制因底层数据结构而异例如concurrent_vector使用generic_range_typeiteratorconcurrent_hash_map使用hash_map_rangeiterator。掌握concurrent_map::range()与 ContainerRange 需求后你可以把这一套容器即区间、算法即调度的思维平移到所有 oneTBB 并发容器上写出结构统一、可扩展的并行代码。【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考