mold 中的线程安全实践:解读 oneTBB Thread Safety 规范及其在链接器中的应用
2026/9/15 12:07:15 网站建设 项目流程

mold 中的线程安全实践:解读 oneTBB Thread Safety 规范及其在链接器中的应用

【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold

导读

本文以当前仓库中 third-party/tbb/doc/main/specification/source/thread_safety.rst 文档为骨架,系统讲解 oneAPI Threading Building Blocks(oneTBB)线程安全模型的两条核心规则,并结合本仓库中 oneTBB 在 mold 链接器(src)里的真实应用场景,深入解析"不同对象可并发、同一对象须互斥"的规则如何落地。读完本文,你将掌握 oneTBB 线程安全契约的准确含义、并发容器等例外情况的使用边界,以及如何从 mold 源码 中验证并运用这些规则写出无数据竞争的并行代码。

线程安全规则的默认契约

thread_safety.rst 开篇即定义 oneTBB 库的默认线程安全约定——除非类描述中另有说明,否则以下两条规则适用于库中的所有方法或函数

  1. 两个线程可以在不同的对象上并发调用同一个方法或函数
  2. 两个线程在同一对象上并发调用方法或函数是不安全的

简而言之:默认情况下 oneTBB 的线程安全边界是"对象级"而非"函数级"的。规则允许同一段代码被多个线程各自作用于自己持有的对象实例上并行执行,但不保证同一实例内部状态被多个线程同时触碰时的正确性。这与 C++ 标准库容器"不同容器实例之间互不干扰、同一容器实例的并发读写是数据竞争"的模型一脉相承,也是并行编程中"状态隔离"思想的直接体现。

例外声明机制

文档明确指出"Departures from this convention are noted in the classes descriptions",即所有偏离上述默认约定的类,都会在各自的类描述中显式标注。这意味着阅读 oneTBB 任何一个类时,应当遵循"先查默认规则,再看是否有例外标注"的流程,而不是想当然地认为某个接口天然支持并发。

并发容器:更宽松的例外

文档紧接着给出最典型的例外——并发容器(concurrent containers)。由于它们的本质设计目标就是支持多线程对同一个容器对象执行某些并发操作,因此其线程安全约定"更自由"(more liberal)。

从 containers.rst 可以看到 oneTBB 并发容器家族覆盖了完整的容器类型谱系:

  • 序列(Sequences)concurrent_vector
  • 队列(Queues)concurrent_queueconcurrent_bounded_queueconcurrent_priority_queue
  • 无序关联容器(Unordered associative)concurrent_hash_mapconcurrent_unordered_mapconcurrent_unordered_multimapconcurrent_unordered_setconcurrent_unordered_multiset
  • 有序关联容器(Ordered associative)concurrent_mapconcurrent_multimapconcurrent_setconcurrent_multiset
  • 辅助类(Auxiliary)tbb_hash_comparenode_handles

需要注意的是,"更自由"不等于"所有操作都并发安全"。以concurrent_hash_map为例,其类文档在 concurrent_hash_map_cls.rst 中明确区分了"并发安全修饰器"与"并发不安全修饰器"两类成员:

// Defined in header <oneapi/tbb/concurrent_hash_map.h> namespace oneapi { namespace tbb { template <typename Key, typename T, typename HashCompare = tbb_hash_compare<Key>, typename Allocator = tbb_allocator<std::pair<const Key, T>>> class concurrent_hash_map { public: using key_type = Key; using mapped_type = T; using value_type = std::pair<const Key, T>; // 并发安全的查找与插入 bool find( const_accessor& result, const key_type& key ) const; bool find( accessor& result, const key_type& key ); bool insert( const_accessor& result, const key_type& key ); bool insert( accessor& result, const key_type& key ); template <typename... Args> bool emplace( accessor& result, Args&&... args ); bool erase( const key_type& key ); // 并发不安全的整体操作 void clear(); void swap( concurrent_hash_map& other ); void rehash( size_type sz = 0 ); size_type size() const; size_type bucket_count() const; // ... }; }} // namespace oneapi::tbb

其中findinsertemplaceerase这类针对单个键的细粒度操作才是文档所说的"同一容器对象上的某些并发操作";而clear()swap()rehash()size()等结构性操作仍然遵循默认规则,不允许与其它操作并发执行。这正是"并发容器更自由"的准确边界。

accessor 机制:并发访问的钥匙

concurrent_hash_map支持并发读写的关键设计是 accessor 与 const_accessor 这一对内嵌类。它们的作用是让多个线程同时、安全地访问容器中的键值对:

  • accessor提供读写访问,const_accessor提供只读访问;
  • 一个 accessor 被称为empty,当它不指向任何元素;
  • 通过find/insert/emplace成功命中后,accessor 持有对应元素的访问权,可通过operator*/operator->解引用;
  • release()释放所有权,析构函数在非空时也会自动释放。
oneapi::tbb::concurrent_hash_map<std::string, int> table; oneapi::tbb::concurrent_hash_map<std::string, int>::accessor acc; if (table.find(acc, "key")) { // 获得读写访问权 acc->second = 42; // 修改映射值 acc.release(); // 释放访问权 } oneapi::tbb::concurrent_hash_map<std::string, int>::const_accessor cacc; if (table.find(cacc, "key")) { // 获得只读访问权 int v = cacc->second; }

从实现层面看,accessor 内部通常持有对应 bucket 的读写锁或引用计数,从而保证"拿到 accessor 后对元素的读改写"与"其它线程的插入/删除"互不干扰——这正是并发容器得以放宽默认线程安全规则的根本原因。值得注意的是,访问器机制仅针对单元素操作,若需要并发遍历整个容器,则应使用range(grainsize)配合并行算法(见下文)。

互斥原语:手动实现"同一对象互斥"

当业务逻辑确实需要在同一个对象上执行复合操作、而该对象又不属于并发容器时,oneTBB 提供了完整的互斥原语族来兜底,见 mutual_exclusion.rst:

  • mutexrw_mutex:通用互斥锁与读写锁
  • spin_mutexspin_rw_mutex:自旋锁与自旋读写锁
  • speculative_spin_mutexspeculative_spin_rw_mutex:基于硬件事务内存(TSX)的投机自旋锁
  • queuing_mutexqueuing_rw_mutex:公平排队锁,避免自旋锁的饿死问题
  • null_mutexnull_rw_mutex:空实现,用于在模板参数中"关闭"锁语义

这些原语与默认线程安全规则互补:规则说"同一对象并发调用不安全",互斥原语则提供把这种不安全转化为安全的工具——通过对临界区加锁,让对同一对象的访问在时间上串行化。选择哪种锁取决于竞争激烈程度、临界区长短与是否容忍线程饿死等权衡,这是 oneTBB 文档中一贯的设计哲学:给出默认规则,再给出按需绕过的机制

在 mold 链接器中的真实落地

本文档所讲述的规则并非抽象教条。当前仓库的 mold 链接器以 oneTBB 为并行运行时(src 目录中大量#include <tbb/...>tbb::调用即为证据),其并行阶段严格遵循"不同对象可并发、同一对象须互斥"的契约。

并行遍历:不同对象上的并发

mold 中大量的并行循环都通过tbb::parallel_for/tbb::parallel_for_each不同的输入对象(如不同的 ObjectFile、InputSection)执行同一处理逻辑,例如 gc-sections.cc:

tbb::parallel_for_each(ctx.objs, & { // 每个线程处理不同的 ObjectFile,互不干扰 ... });

每个 lambda 只读写自己负责的那个对象实例,完全符合规则第一条"两个线程可以在不同的对象上并发调用同一方法"。类似的模式还出现在 arch-arm32.cc、arch-ppc64v1.cc、gdb-index.cc 等多处。

共享结果汇聚:需要并发容器的场合

当并行结果必须写入同一个共享结构时,mold 便切换到并发容器这一例外机制。例如 mapfile.cc 用concurrent_hash_map记录符号归属:

#include <tbb/concurrent_hash_map.h> tbb::concurrent_hash_map<InputSection<E> *, std::vector<Symbol<E> *>> symtab;

多个线程同时对该容器执行insert/find,正是利用了concurrent_hash_map支持同一对象并发单元素操作的特性。又如 mold.h 中的未定义符号错误汇总:

tbb::concurrent_hash_map<Symbol<E> *, std::vector<std::string>> undef_errors;

以及 gc-sections.cc 中段名到段集合的映射:

tbb::concurrent_unordered_map<std::string_view, tbb::concurrent_vector<InputSection<E> *>> map;

icf.cc 的等价类合并则使用了concurrent_unordered_multimap

tbb::concurrent_unordered_multimap<InputSection<E> *, InputSection<E> *> map;

这些容器被多个工作线程并发insert、并发遍历,正是文档所述"并发容器允许对同一容器对象执行某些并发操作"的典型落地。读者若想验证其正确性边界,可以对照 concurrent_hash_map_cls.rst 中"Concurrently unsafe modifiers"一节的声明——这些容器在 mold 中的使用均避开了clear/swap/rehash与其它操作的并发组合。

线程私有状态:enumerable_thread_specific

除了并发容器,oneTBB 还通过enumerable_thread_specific提供线程私有存储,把"不同对象并发"的规则贯彻到极致:每个线程看到的是自己独有的副本,天然无竞争。mold 在 icf.cc、passes.cc、output-chunks.cc、thunks.cc 中多处使用,例如:

tbb::enumerable_thread_specific<i64> num_classes; // 每个线程独立的计数器

每个线程只读写自己的局部副本,最后再归并,完全绕开了"同一对象"的竞争问题。

归并扫描:parallel_scan 的用法

output-chunks.cc 与 gdb-index.cc 展示了tbb::parallel_scan配合tbb::blocked_range做前缀和/归并的用法——这是一种"以并行方式安全地聚合共享结果"的经典模式,其结果也印证了:oneTBB 的线程安全模型不仅包含"锁与容器",还包含"算法层面的结构化解竞争"。

实践要点与自查清单

综合文档与源码,将 oneTBB 线程安全模型落地到自己的并行代码时,建议遵循以下要点:

  1. 先默认后例外:任何 oneTBB 类,先假设"同一对象并发不安全",再去类文档中查找是否标注了更宽松的约定(如并发容器);
  2. 区分细粒度与结构性操作:即便使用并发容器,也只并发执行文档明确允许的操作(如find/insert/emplace/erase),clear/swap/rehash/size等仍须串行;
  3. 用 accessor 做读改写:对concurrent_hash_map元素的读-改-写必须通过accessor/const_accessor获得访问权,避免"检查后修改"的 TOCTOU 竞争;
  4. 共享结果交给并发容器或线程私有存储:参考 mold 的做法——需要共享写入时用并发容器,需要隔离时用enumerable_thread_specific,需要结构化解竞争时用parallel_scan等并行算法;
  5. 同一对象的复合操作必须加锁:当并发容器无法覆盖需求时,从 mutual_exclusion.rst 的互斥原语族中选择合适的锁(普通互斥、读写锁、自旋锁或排队锁)保护临界区。

延伸阅读

  • 线程安全总则原文:thread_safety.rst
  • 并发容器家族总览:containers.rst
  • concurrent_hash_map完整接口:concurrent_hash_map_cls.rst 与 accessors.rst
  • 互斥原语族:mutual_exclusion.rst
  • mold 中的落地实例:gc-sections.cc、icf.cc、mapfile.cc、mold.h、output-chunks.cc、gdb-index.cc

【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询