尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

oneapi::tbb::concurrent_map 并发安全修改器(safe modifiers)完全指南:insert、emplace 与 merge 的并发语义与实现剖析

oneapi::tbb::concurrent_map 并发安全修改器(safe modifiers)完全指南:insert、emplace 与 merge 的并发语义与实现剖析 oneapi::tbb::concurrent_map 并发安全修改器safe modifiers完全指南insert、emplace 与 merge 的并发语义与实现剖析【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold本篇技术指南以 oneAPI Threading Building BlocksoneTBB官方规范文档 safe_modifiers.rst 为主体系统讲解oneapi::tbb::concurrent_map中所有可并发安全执行的内容修改操作单值插入、序列插入、节点句柄插入、就地构造与容器合并。结合本仓库 vendored 的 concurrent_map.h 源码你将掌握每个重载的语义、返回值、前置条件与性能特性并了解 mold 链接器如何利用 oneTBB 并发容器实现多线程并行链接。一、什么是 Concurrently safe modifiersconcurrent_map是 oneTBB 提供的有序关联容器存储唯一键元素支持并发的插入insert、查找lookup与遍历traversal但不支持并发删除见 concurrent_map_cls.rst。Concurrently safe modifiers 一节定义的全部成员函数可以与彼此、与查找方法、以及与容器遍历并发执行。这意味着任意数量的线程可以同时调用本节任何一个修改函数包括相同函数这些调用可以与find、count、contains、lower_bound、upper_bound、equal_range等查找操作并发这些调用可以与遍历操作迭代器遍历、range()并行区间遍历并发迭代器在并发修改下不会失效——你持有的迭代器始终指向稳定存在的元素。对比之下容器还提供了一组并发不安全的修改器clear、unsafe_erase、unsafe_extract、swap见 unsafe_modifiers.rst这些函数只能串行执行一旦与其他方法并发执行行为未定义undefined behavior。因此在多线程场景中务必只使用本节safe modifiers定义的接口。从实现上看concurrent_map继承自 oneTBB 的并发跳表concurrent_skip_list底层采用map_traitsKey, Value, KeyCompare, geometric_level_generator32, Allocator, false配置AllowMultimapping false表示键唯一插入操作通过在跳表各层原子地维护前驱/后继指针完成这正是其并发安全的根基见 concurrent_map.h。二、插入单个值insert的六种重载2.1 拷贝插入std::pairiterator, bool insert( const value_type value );尝试将值value插入容器。返回值std::pairiterator, bool。若插入成功iterator指向新插入的元素若容器中已存在键相等的元素则指向该已有元素且此时bool为false插入实际发生时bool为true。前置条件value_type必须满足 ISO C 标准 [container.requirements] 一节中CopyInsertable的要求。带提示位置的拷贝插入iterator insert( const_iterator hint, const value_type other );同样尝试插入value可选地使用hint参数作为元素应放置位置的建议。注意hint只是性能提示——即使hint给出错误位置插入依然正确完成只是可能损失一点性能。返回值仅为iterator指向插入的元素或键相等的已有元素不再携带bool。2.2 异质转发插入template typename P std::pairiterator, bool insert( P value ); template typename P iterator insert( const_iterator hint, P value );这两个模板重载分别等价于emplace(std::forwardP(value))与emplace_hint(hint, std::forwardP(value))。它们仅在std::is_constructiblevalue_type, P::value为true时参与重载决议SFINAE 约束从而避免与其它重载产生歧义。在 concurrent_map.h 的源码中这两个重载正是通过std::enable_ifstd::is_constructible实现上述约束并将参数完美转发给emplace/emplace_hint。这允许直接传入std::pairconst Key, T的构造参数而避免不必要的拷贝。2.3 移动插入std::pairiterator, bool insert( value_type value ); iterator insert( const_iterator hint, value_type other );使用移动语义尝试插入值。value在插入后被留在有效但未指定valid but unspecified的状态——这是标准移动操作的标准契约。若插入失败键已存在value同样会被移动消耗吗规范只说明插入成功后value处于有效但未指定状态实际实现中由于跳表需要将节点链接进结构即使键冲突失败传入对象也可能已被移动构造到临时节点中。因此不要依赖失败后原对象的内容。前置条件value_type必须满足MoveInsertable要求[container.requirements]。2.4 使用建议// 典型多线程插入返回值中的 bool 用于判断谁赢得了首次插入 std::pairconcurrent_mapstring,int::iterator, bool res table.insert({key, value}); if (res.second) { // 本线程完成了插入 } else { // 键已存在res.first 指向已有元素 }当一次并发插入只需要存在一个元素即可、重复操作可丢弃时利用返回的bool判断插入是否真正发生是使用concurrent_map最经典的范式。三、插入元素序列template typename InputIterator void insert( InputIterator first, InputIterator last ); void insert( std::initializer_listvalue_type init );区间版本尝试将半开区间[first, last)中的所有元素插入容器。若区间内存在多个键相等的元素插入哪一个是不确定的unspecified。initializer_list版本等价于insert(init.begin(), init.end())。前置条件InputIterator必须满足 ISO C 标准 [input.iterators] 中InputIterator的要求。该重载不返回任何值void。注意区间/初始化列表插入内部的逐元素插入是并发安全的但该成员本身通常用于在构造/初始化阶段批量填充容器或由单线程调用以避免竞态时的不确定性。四、插入节点句柄零拷贝的node_type插入std::pairiterator, bool insert( node_type nh ); iterator insert( const_iterator hint, node_type nh );节点句柄node handle是 oneTBB 并发关联容器concurrent_map、concurrent_multimap、concurrent_set、concurrent_multiset以及对应 unordered 系列共有的 move-only 嵌套类型通过container::node_type暴露。它代表脱离容器实例存在的一个节点允许读取和修改节点内数据并可将节点插入兼容的容器实例详见 node_handles_cls.rst。语义要点若nh为空empty则什么都不做否则尝试将nh拥有的节点插入容器。插入失败键已存在时nh继续保持对节点的所有权插入成功时nh被置为空状态不会执行value_type的任何拷贝或移动构造函数——节点以原生链接方式转移nh非空且get_allocator() ! nh.get_allocator()时行为未定义两个容器的分配器必须兼容返回值规则与普通插入一致pair版本的bool指示插入是否发生iterator指向插入的元素或键与nh.key()等价的已有元素带hint的版本仅返回iterator。节点句柄通常通过unsafe_extract从容器中取出节点而产生见 unsafe_modifiers.rst。因此典型用法是串行阶段用unsafe_extract拆出节点再在并行阶段用insert(node_type)安全地转移进目标容器全程零拷贝、零分配。节点句柄为空、赋值与析构时的资源释放规则、以及key()/mapped()的访问契约均可参见 node_handles_cls.rst。五、就地构造emplace与emplace_hinttemplate typename... Args std::pairiterator, bool emplace( Args... args ); template typename... Args iterator emplace_hint( const_iterator hint, Args... args );emplace从args就地构造一个元素并尝试插入。返回值与insert的pair版本一致bool指示插入是否发生iterator指向插入的元素或键相等的已有元素。emplace_hint额外接受hint作为放置位置建议仅返回iterator。前置条件value_type必须满足EmplaceConstructible要求[container.requirements]。emplace是比insert更高效的插入方式它直接在节点内存中构造std::pairconst Key, T避免了先构造value_type再拷贝/移动的开销。典型用法// 无需先构造临时 pair直接就地构造 auto res table.emplace(std::piecewise_construct, std::forward_as_tuple(key), std::forward_as_tuple(42));注意由于容器键不可变value_type是std::pairconst Key, T即使元素构造完成但最终插入失败键冲突已构造的临时节点也会被销毁bool返回false。六、合并容器mergetemplate typename SrcCompare void merge( concurrent_mapKey, T, SrcCompare, Allocator source ); template typename SrcCompare void merge( concurrent_mapKey, T, SrcCompare, Allocator source ); template typename SrcCompare void merge( concurrent_multimapKey, T, SrcCompare, Allocator source ); template typename SrcCompare void merge( concurrent_multimapKey, T, SrcCompare, Allocator source );merge将source中键在目标容器中不存在的那些元素转移到目标容器与concurrent_multimap允许重复键合并时若源中有多个键相等的元素转移哪一个是不确定的不执行value_type的任何拷贝或移动构造函数——元素以节点链接方式整体转移本质上是节点句柄机制的批量版本get_allocator() ! source.get_allocator()时行为未定义目标容器*this的模板参数Compare可以与source的SrcCompare不同因此支持不同比较器的容器之间合并这正是merge模板化SrcCompare的原因。源码中四个重载均委托给this-internal_merge(...)concurrent_map.hinternal_merge在底层跳表实现中通过遍历源节点、原子解除链接并链入目标来实现零拷贝转移。由于目标容器中不存在的键才会被转移merge天然适用于去重归并场景多个线程各自构建局部concurrent_map最后统一merge进全局容器重复键自动被丢弃无需额外判重逻辑。七、在 mold 链接器中的真实应用oneTBB 并发容器如何支撑并行链接当前仓库 mold 是一个现代高速链接器其并行化基石正是 oneTBB。虽然 mold 的核心符号表使用自研的开放寻址哈希表ConcurrentMap见 lib.h以 32 字节对齐的Entry减少缓存行伪共享但整个链接流程广泛依赖 oneTBB 的并行算法与并发容器gc-sections.cc 引入tbb::concurrent_unordered_map、tbb::concurrent_vector与tbb::parallel_for_each并行执行段回收--gc-sections中各对象文件依赖图的遍历icf.cc 使用tbb::concurrent_unordered_map、tbb::concurrent_vector、tbb::enumerable_thread_specific、tbb::parallel_for、tbb::parallel_for_each与tbb::parallel_sort并行执行相同代码折叠ICF的哈希分组与去重mold.h 中Context持有大量tbb::concurrent_vector池对象文件池、共享库池、段池、字符串池等供各工作线程无锁追加arch-ppc64v1.cc 使用ctx.symbol_map.parallel_for_each并行遍历符号表arch-arm32.cc 使用tbb::parallel_for并行处理重定位条目。在 mold 的并行模型中每个线程处理一批输入文件再把结果并发写入共享结构正是 oneTBB 并发容器的典型应用模式。本节讨论的concurrent_map的insert/emplace/merge语义就是这类并行归并场景的正确打开方式利用insert返回的bool判断首次插入利用merge完成无重复键的零拷贝归并。八、并发安全使用清单与常见误区使用清单操作是否并发安全说明insert全部 6 个重载 序列/initializer_list✅可与查找、遍历及其它 safe modifiers 并发emplace/emplace_hint✅就地构造 插入insert(node_type)2 个重载✅零拷贝节点转移分配器必须兼容merge4 个重载✅批量零拷贝转移键冲突自动丢弃clear/unsafe_erase/unsafe_extract/swap❌只能串行执行并发执行行为未定义常见误区混淆at/operator[]与插入语义operator[]在键不存在时会就地emplace一个默认构造的mapped_type并返回引用见 concurrent_map.h这本身是并发安全的但它隐含写入语义且返回的引用在并发场景下仍需外部同步保护。依赖hint的准确性hint仅为性能建议传错位置不导致错误只是可能降低插入效率并发插入时 hint 的价值有限因为其他线程可能随时改变局部布局。跨分配器转移节点节点句柄插入与merge均要求分配器兼容get_allocator() nh.get_allocator()/source.get_allocator()违反即未定义行为。把 unsafe 系列当 safe 用unsafe_erase、unsafe_extract、clear、swap与其它方法并发执行时行为未定义这是unsafe前缀的含义与std::unordered_map中erase的并发语义完全不同。移动插入失败后的状态移动版insert之后无论成功与否value都可能处于有效但未指定状态不要假定失败时原值保持不变。九、总结oneapi::tbb::concurrent_map的并发安全修改器覆盖了单值插入拷贝/移动/异质转发、序列插入、节点句柄零拷贝插入、就地构造emplace以及跨容器零拷贝merge五类操作全部可与查找、遍历及其它修改操作并发执行。其正确用法建立在两个核心契约之上利用返回值判断插入是否实际发生以及只使用 safe modifiers 进行并发修改。配合 concurrent_map.h 的跳表实现与 node_handles_cls.rst 的节点句柄规范你可以在自己的多线程程序中安全高效地组织并行计算、零拷贝归并的数据流正如 mold 链接器在段回收、ICF 折叠与符号表构建等阶段所做的那样。【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表