ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

深入解析 oneTBB `enumerable_thread_specific` 的并发安全修饰符:`local()` 与线程本地元素访问机制

深入解析 oneTBB `enumerable_thread_specific` 的并发安全修饰符:`local()` 与线程本地元素访问机制 深入解析 oneTBBenumerable_thread_specific的并发安全修饰符local()与线程本地元素访问机制【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold导读oneapi::tbb::enumerable_thread_specific以下简称 ETS是 oneTBB 提供的线程本地存储Thread Local StorageTLS容器模板。本文聚焦于该容器中“Concurrently safe modifiers”可并发安全调用的修饰符一组接口——local()与local(bool exists)说明它们的语义、行为边界、底层哈希表实现以及它们与 ETS 其他成员函数构造、大小查询、迭代、合并之间的配合方式。读者将掌握在多线程并行区间内安全读写线程私有元素的正确姿势理解并发安全在源码层面是如何通过无锁哈希表与原子操作保障的并看到 oneTBB 在当前项目mold 链接器中的真实落地场景。一、概述什么是并发安全修饰符在 oneTBB 规范文档中enumerable_thread_specific的成员函数被明确划分为几个类别见 enumerable_thread_specific_cls.rst 的 Member functions 目录树construct_destroy_copy构造、析构、拷贝与赋值safe_modifiers本文主题——local()与local(bool)unsafe_modifiersclear()等不允许并发调用的操作size_and_capacitysize()、empty()iterationbegin()、end()、range()combiningcombine()、combine_each()。本文对应的规范文件为 safe_modifiers.rst其开篇给出了一条总纲性约定All member functions in this section can be performed concurrently with each other.即本节中的所有成员函数可以彼此并发执行——多个线程可以同时对同一个 ETS 实例调用local()互不阻塞、互不破坏数据结构。这是 ETS 区别于普通thread_local关键字以及标准库std::thread_local存储的关键价值它既是线程本地的又是可枚举enumerable的容器。二、reference local()获取当前线程对应元素的引用2.1 规范语义local()是 ETS 最核心的访问入口规范定义如下如果当前线程没有对应的元素则本方法会新构造一个元素。若构造*this时提供了 exemplar样板对象则新元素以拷贝构造方式创建否则以默认构造方式创建。返回*this中与当前线程对应的元素的引用。对应到 safe_modifiers.rstreference local();关键点懒创建lazy creationETS 刚构造完成时不包含任何元素。只有当某个线程第一次调用local()时该线程对应的元素才被创建见 enumerable_thread_specific_cls.rst 中The thread-local elements are created lazily的描述。这意味着零线程使用时 ETS 的开销接近于零。元素个数 ≠ 应用线程数ETS 中的元素数量等于实际调用过local()的不同线程数而非应用在用的线程总数。元素的构造方式由构造函数决定无参构造 → 默认构造传 exemplarconst T或T→ 拷贝构造传finit函数对象 → 以finit()的结果拷贝构造传可变参数args...→ 以T(args...)构造参见 construct_destroy_copy.rst。2.2 源码实现local()是local(bool)的薄包装在头文件 enumerable_thread_specific.h 中可以看到两者的真实实现//! returns reference to local, discarding exists reference local() { bool exists; return local(exists); } //! Returns reference to calling threads local copy, creating one if necessary reference local(bool exists) { void* ptr this-table_lookup(exists); return *(T*)ptr; }也就是说local()通过local(bool)完成工作它向底层哈希表发起一次table_lookup查询随后将返回的裸指针转换为T返回给调用者。exists标志只是被丢弃了。2.3 底层机制无锁开放寻址哈希表table_lookup在 enumerable_thread_specific.h 附近实现其数据结构是ets_base中一张开放寻址open addressing的哈希表void* ets_baseETS_key_type::table_lookup( bool exists ) { const key_type k ets_key_selectorETS_key_type::current_key(); __TBB_ASSERT(k ! key_type(), nullptr); void* found; std::size_t h std::hashkey_type{}(k); for( array* r my_root.load(std::memory_order_acquire); r; r r-next ) { std::size_t mask r-mask(); ... } }几个值得注意的底层细节均可从 enumerable_thread_specific.h 的ets_base结构验证哈希表的slot由std::atomickey_type key与void* ptr组成第 126-136 行claim()通过compare_exchange_strong完成对空槽位的原子抢占哈希表由一串大小逐级减半的数组array 链表构成根节点my_root与元素计数my_count均为原子变量第 142-143 行查找与插入全程无锁多个线程并发local()时通过原子 CAS 竞争空槽从而满足可彼此并发执行的规范要求。2.4 潜在陷阱线程 ID 的复用规范中特别给出了 caution警告见 enumerable_thread_specific_cls.rstenumerable_thread_specific使用std::this_thread::get_id()返回的 OS 特定值来标识线程。该值只在线程存活期内保证唯一新创建的线程可能拿到与已销毁线程相同的 OS 线程 ID。由此可推出两个行为后果ETS 的元素数量可能小于实际调用过local()的不同线程数不同线程 ID 被复用合并某线程首次调用local()时拿到的元素可能不是新构造的它命中了被复用 ID 留下的旧元素。这是使用 ETS 做精确计数如每线程独立计数器再求和时必须留意的语义边界size()反映的是不同 key 的数量而非线程的峰值数。三、reference local(bool exists)带存在性探测的访问3.1 规范语义reference local( bool exists );与local()类似但多了一个输出参数exists若当前线程的元素已经存在exists被置为true否则置为false。返回当前线程对应元素的引用。这一接口的价值在于它允许调用方区分这是新创建的元素还是复用已有元素从而避免对元素内容做不必要的初始化覆盖。例如第一次访问时需要填充种子值后续访问则直接累加——exists让调用方免于额外维护一个是否已初始化的标志。3.2 实现位置与语义来源local(bool)的实现直接委托给table_lookup(exists)见上文 2.2 节源码。exists的真实来源是哈希表查找结果若在表中找到匹配当前线程 key 的槽位 →exists true返回已有元素指针若未找到 → 构造新元素、原子抢占空槽写入 →exists false返回新元素指针。因此exists的语义与size()的语义保持一致ETS 的元素存在与否以哈希表中的 key 为准。3.3 典型使用模式结合local(bool)的语义一个典型的每线程惰性初始化模式如下#include oneapi/tbb/enumerable_thread_specific.h #include oneapi/tbb/parallel_for.h // 每线程持有一个 std::vectorint 作为累加缓冲区 tbb::enumerable_thread_specificstd::vectorint buffers( std::vectorint{ /* exemplar */ }); tbb::parallel_for(0, N, { bool exists false; auto buf buffers.local(exists); if (!exists) { buf.reserve(1024); // 仅首次访问时执行一次 } buf.push_back(i); });注意exists的检查与元素修改位于同一线程内、同一临界逻辑中不会与其他线程的local()调用产生竞态跨线程的并发安全由 ETS 的哈希表无锁机制保证而元素本身的修改则需要T自身满足线程隔离每线程只碰自己的元素天然无冲突。四、与 ETS 其他接口的联动一张完整的地图local()家族只是 ETS 的一个侧面。为了让读者在实战中不踩坑下面把与之紧密相关的接口串起来完整类声明见 enumerable_thread_specific_cls.rst。4.1 模板签名与ETS_key_usage_typetemplate typename T, typename Allocator cache_aligned_allocatorT, ets_key_usage_type ETS_key_type ets_no_key class enumerable_thread_specific;第三个模板参数ETS_key_usage_type决定底层实现策略共三档规范原文见 enumerable_thread_specific_cls.rst 的 Non-member types and constants 一节枚举值底层 key 消耗说明ets_key_per_instance每个 ETS 实例消耗 1 个原生 TLS key原生 TLS key 数量可能受限且很少ets_no_key默认不消耗原生 TLS key若不指定该参数即使用此默认值ets_suspend_aware使用挂起点suspend point作为 key避免task::suspend改变 ETS 值的问题local()的值可能对不同线程相同但两个不同线程不会同时访问同一个值从源码看key 选择器定义在 enumerable_thread_specific.h默认ets_key_selector以std::thread::id即std::this_thread::get_id()返回值作为 keyets_suspend_aware特化则以suspend_point作为 key这正是支持可挂起任务resumable task场景的实现基础。4.2 构造方式决定local()的元素如何诞生local()创建新元素的方式取决于构造 ETS 时使用的构造函数细节见 construct_destroy_copy.rst构造函数local()首次创建元素的方式enumerable_thread_specific()默认构造templatetypename Finit explicit enumerable_thread_specific(Finit finit)以finit()的返回值为样板拷贝构造finit()必须可被多线程并发求值且每次创建元素时都会调用explicit enumerable_thread_specific(const T exemplar)从 exemplar 拷贝构造explicit enumerable_thread_specific(T exemplar)内部可移动存储 exemplar但线程本地元素始终拷贝构造templatetypename... Args enumerable_thread_specific(Args... args)以T(args...)构造若首参是T、enumerable_thread_specificT或foo()可调用则不参与重载决议对应源码见 enumerable_thread_specific.h五种构造函数分别包装了construct_by_default、construct_by_finit、construct_by_exemplar、construct_by_args等回调叶节点统一挂到my_construct_callback上供local()首次创建元素时回调。4.3 查询与清理size()返回*this中的元素个数等于构造或最近一次clear()之后调用过local()的不同线程数size_and_capacity.rst源码中即return my_locals.size();enumerable_thread_specific.h。empty()容器无元素时返回true。clear()属于unsafe_modifiers会清空所有线程本地元素且不允许与local()等操作并发调用文档分类见 enumerable_thread_specific_cls.rst 的 toctree其声明见 enumerable_thread_specific.h。4.4 汇总与迭代combine(f)/combine_each(f)对全部线程本地元素做归约见同目录 combining.rst。range(grainsize)、begin()/end()ETS 可作为容器被并行算法或迭代器遍历iteration.rst。典型的两阶段模式是并行阶段各线程只调local()写自己的元素 → 串行/并行阶段用combine_each归约。local()的安全并发性正是这一模式成立的前提。五、实测验证测试用例与项目内真实用法5.1 测试用例中的local()使用oneTBB 自带的单元测试 test_enumerable_thread_specific.cpp 大量覆盖了local()路径。例如第 378 行typename CounterBigType::reference my_local MyCounters.local();在多线程压力测试stress_local一类的例程中直接以local()返回值作为累加目标第 382 行MyCounters2.local()用于验证自定义分配器allocator场景下local()返回元素的对齐与内容正确性。这些用例佐证了规范语义local()可以在并行环境下反复调用并安全读写。5.2 当前项目mold中的真实落地本文所述 ETS 并非纸上谈兵——在当前仓库mold 链接器的源码中就存在两处典型使用passes.cc用法一避免锁竞争的分线程缓存在创建输出段output section并为输入段分配归属的并行循环中passes.ccusing MapType std::unordered_mapOutputSectionKey, OutputSectionE *, OutputSectionKey::Hash; MapType map; std::mutex mu; bool ctors_in_init_array has_ctors_and_init_array(ctx); tbb::enumerable_thread_specificMapType caches; // Make a per-thread cache of the main map to avoid lock contention. tbb::parallel_for((i64)0, (i64)ctx.objs.size(), { ... });每个工作线程通过 ETS 持有自己的MapType缓存避免了对共享std::mutex的高频竞争——这正是local()每线程独立元素 能力的直接收益。用法二并行累加.dynstr段长度在计算.dynstr段大小的并行循环中passes.cctbb::enumerable_thread_specifici64 size; tbb::parallel_for((i64)1, (i64)syms.size(), { syms[i]-aux-dynsym_idx i; size.local() syms[i]-name().size() 1; });每个线程用size.local()累加各自处理到的符号名长度最终再合并后续代码对size各元素求和——这是local() 归约的最简洁示范local()返回的引用可直接用于复合赋值。两处用法的共性local()与parallel_for天然搭配把每线程私有累加器/缓存从手写std::mutex 线程 ID 映射中解放出来。六、使用建议与边界提醒基于规范与源码给出如下实践建议均为仓库可验证的结论不涉及任何外部资料local()并发安全但元素内容的操作要遵守每线程只碰自己的元素约定规范保证local()调用本身彼此并发安全哈希表无锁、原子 CAS 抢占但同一线程多次local()返回的是同一引用跨线程不应共享该引用去写。需要知道是否新建元素时用local(bool)首次访问做一次性初始化如reserve、填充种子是它的典型场景无此需求时用local()更简洁。不要依赖size()精确等于线程数由于 OS 线程 ID 可能复用size()只保证小于等于调用过local()的不同线程数规范 caution 已明示。clear()是 unsafe 操作必须在所有并发local()调用结束后执行否则属于未定义行为文档分类见 enumerable_thread_specific_cls.rst。需要与可挂起任务task::suspend配合时务必指定ets_suspend_aware否则挂起/恢复可能改变 ETS 的取值其代价是local()值可能在不同线程间相同但任意时刻只允许一个线程访问该值。元素类型的构造代价需评估local()首次调用会按构造函数语义创建元素拷贝构造或默认构造元素类型昂贵时建议使用Texemplar 或finit构造避免反复拷贝。结语enumerable_thread_specific::local()与local(bool exists)是 oneTBB 线程本地存储的访问中枢规范用一句话定义了它们的并发安全性而 enumerable_thread_specific.h 中的无锁哈希表与原子槽位抢占则提供了这句话的工程支撑。配合构造、大小查询、归约与迭代接口ETS 能够以容器视角管理线程私有数据并在 mold 链接器这样追求极致并行性能的项目中直接落地。理解local()的语义与边界是正确使用 ETS 的第一步也是深入并行编程的一把钥匙。【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表