mold 中的并发基石:深入解读 oneTBB 互斥锁(Mutex)家族规范与源码实现
2026/9/14 18:34:53 网站建设 项目流程

mold 中的并发基石:深入解读 oneTBB 互斥锁(Mutex)家族规范与源码实现

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

本指南以仓库third-party/tbb中 oneAPI Threading Building Blocks(oneTBB)官方规范文档《Mutual Exclusion》为主体,系统讲解 oneTBB 提供的 10 种互斥锁类(mutexrw_mutexspin_mutexspin_rw_mutexspeculative_spin_mutexspeculative_spin_rw_mutexqueuing_mutexqueuing_rw_mutexnull_mutexnull_rw_mutex)的接口契约、公平性/可重入性保证与适用场景,并结合 mutex.h 的源码实现剖析其“自适应自旋 + 等待通知”的底层机制。读完本文,你将能看懂 TBB 互斥锁的完整规范,理解scoped_lock作用域锁模式与三类编译期 traits,并掌握在 mold 这类高性能链接器项目中如何为不同临界区挑选合适的锁。

一、总览:互斥锁家族解决什么问题

oneTBB 提供了一组互斥原语,用于编写无数据竞争(race-free)的线程安全代码。一个互斥对象(mutex)用于防止数据竞争,并在线程之间提供安全的数据同步。规范文档原文见 mutual_exclusion.rst,其中列出的互斥类共 10 个,按职责可分为两组:

  • 互斥(Mutex)类mutexspin_mutexspeculative_spin_mutexqueuing_mutexnull_mutex
  • 读写锁(ReaderWriterMutex)类rw_mutexspin_rw_mutexspeculative_spin_rw_mutexqueuing_rw_mutexnull_rw_mutex

每个类的完整接口说明位于 mutual_exclusion/ 目录下对应的*_cls.rst文档中。这些类共同点在于:全部不可拷贝、不可移动,且普遍不公平、不可重入null_*系列除外)。

二、规范基石:Mutex 与 ReaderWriterMutex 命名需求

要读懂 10 个类,必须先理解它们共同建模的两个“命名需求”(Named Requirements)。

2.1 Mutex 需求与 scoped_lock 作用域锁模式

Mutex 需求文档 指出:互斥锁与锁的接口“相对精简(spartan),专为高性能设计”,并强制推行scoped locking pattern(作用域锁模式),这是 C++ 库中广泛使用的模式,原因是:

  • 无需记得手动释放锁;
  • 当异常从互斥保护区域抛出时,锁会自动释放。

模式由两部分组成:一个mutex对象;一个lock对象——构造 lock 对象即获得 mutex 上的锁,销毁 lock 对象即释放锁。规范给出的示例:

{ // Construction of myLock acquires lock on myMutex M::scoped_lock myLock( myMutex ); // ... actions to be performed while holding the lock ... // Destruction of myLock releases lock on myMutex }

若区域内操作抛出异常,锁会随代码块退出而自动释放。

一个类型M满足 Mutex 需求,需要具备如下scoped_lock接口(见规范原文):

class M { // Implementation specifics // ... class scoped_lock { public: constexpr scoped_lock() noexcept; scoped_lock(M& m); ~scoped_lock(); scoped_lock(const scoped_lock&) = delete; scoped_lock& operator=(const scoped_lock&) = delete; void acquire(M& m); bool try_acquire(M& m); void release(); }; };

各成员语义:

  • M::scoped_lock():构造scoped_lock,不获取互斥锁;
  • M::scoped_lock(M&):构造scoped_lock并在给定互斥锁上取得锁;
  • M::~scoped_lock():释放已获取的锁(若已获取);
  • void acquire(M&):在给定互斥锁上获取锁;
  • bool try_acquire(M&):尝试在给定互斥锁上获取锁,成功返回true,否则false
  • void release():释放已获取的锁。

此外,Mutex 类型还必须定义三个编译期 traits:

  • static constexpr bool M::is_rw_mutex:是否为读写互斥锁;
  • static constexpr bool M::is_recursive_mutex:是否可重入(recursive);
  • static constexpr bool M::is_fair_mutex:是否公平(fair)。

规范明确:互斥类型与M::scoped_lock类型既不可拷贝也不可移动。同时留有一个实现自由度:表中标注为否定保证(如“不公平”“不可重入”)的条目,实现可以给出相反的肯定保证(即“更优”实现是允许的)。

规范还给出建模 Mutex 需求类的保证总表:

Fair(公平)Reentrant(可重入)
mutexNoNo
spin_mutexNoNo
speculative_spin_mutexNoNo
queuing_mutexYesNo
null_mutexYesYes

2.2 ReaderWriterMutex 需求:读写锁语义

ReaderWriterMutex 需求文档 在 Mutex 需求基础上扩展出读写锁概念:引入布尔参数writewrite = true表示申请写锁,write = false表示申请读锁。当一个 ReaderWriterMutex 未被写锁持有时,可以同时持有多个读锁;写锁则排斥所有其他线程同时持锁。

scoped_lock接口在 Mutex 需求之上增加了write参数与升级/降级操作:

class RWM { class scoped_lock { public: constexpr scoped_lock() noexcept; scoped_lock(RWM& m, bool write = true); ~scoped_lock(); scoped_lock(const scoped_lock&) = delete; scoped_lock& operator=(const scoped_lock&) = delete; void acquire(RWM& m, bool write = true); bool try_acquire(RWM& m, bool write = true); void release(); bool upgrade_to_writer(); bool downgrade_to_reader(); }; };

关键成员语义:

  • scoped_lock(RWM&, bool write = true):构造并获取锁,writetrue取写锁,否则取读锁;
  • acquire/try_acquire:同 Mutex 版,但多出write参数决定读写属性;
  • release():释放锁,若未持锁则行为未定义(undefined);
  • bool upgrade_to_writer():把读锁升级为写锁。若锁曾被释放并重新获取则返回false;否则返回true(包括本来就持有写锁的情形);
  • bool downgrade_to_reader():把写锁降级为读锁。若锁曾被释放并重新获取则返回false;否则返回true(包括本来就是读锁的情形)。

同样需要is_rw_mutexis_recursive_mutexis_fair_mutex三个 traits。建模 ReaderWriterMutex 需求类的保证总表:

Fair(公平)Reentrant(可重入)
rw_mutexNoNo
spin_rw_mutexNoNo
speculative_spin_rw_mutexNoNo
queuing_rw_mutexYesNo
null_rw_mutexYesYes

规范附注两个对当前所有读写锁都成立的实现事实:is_recursive_mutex均为falsescoped_lock::downgrade_to_reader恒返回true

三、互斥(Mutex)类逐个详解

3.1mutex:自适应互斥锁

文档见 mutex_cls.rst。mutex自适应(adaptive)方式建模 Mutex 需求:无法获取锁的线程会先自旋(spin),自旋一定时间后再阻塞(block)。它满足 ISO C++ 标准 [thread.mutex.requirements] 的全部互斥需求,但不公平、不可重入。声明于头文件<oneapi/tbb/mutex.h>

namespace oneapi { namespace tbb { class mutex { public: mutex() noexcept; ~mutex(); mutex(const mutex&) = delete; mutex& operator=(const mutex&) = delete; class scoped_lock; void lock(); bool try_lock(); void unlock(); static constexpr bool is_rw_mutex = false; static constexpr bool is_recursive_mutex = false; static constexpr bool is_fair_mutex = false; }; } }

成员函数语义:

  • mutex():以未锁定状态构造互斥锁;
  • ~mutex():销毁一个未锁定的互斥锁(持锁销毁属未定义行为);
  • void lock():获取锁;采用自适应等待逻辑,忙等一定时间后转入阻塞;
  • bool try_lock():非阻塞尝试获取锁,成功返回true,失败返回false
  • void unlock():释放当前线程持有的锁。

3.2spin_mutex:纯自旋互斥锁

文档见 spin_mutex_cls.rst。spin_mutex自旋锁建模 Mutex 需求,同样满足 ISO C++ [thread.mutex.requirements],且不公平、不可重入。声明于<oneapi/tbb/spin_mutex.h>,接口与mutex完全一致(lock/try_lock/unlock+ 三个falsetraits)。其lock()语义为:若锁已被占用则持续自旋try_lock()非阻塞尝试,成功返回true。它适合临界区极短、持锁时间远小于线程切换开销的场景——把 CPU 时间花在自旋上比让出线程更划算。

3.3speculative_spin_mutex:基于硬件事务内存的推测执行锁

文档见 speculative_spin_mutex_cls.rst。该锁同样建模 Mutex 需求,在支持硬件事务内存(HTM)的处理器上(如 Intel® Transactional Synchronization Extensions,Intel® TSX),实现允许无冲突地并发执行受保护数据的修改;不支持 HTM 时退化为普通自旋锁行为。声明于<oneapi/tbb/spin_mutex.h>(与spin_mutex同一头文件),类体不含lock/try_lock/unlock的直接声明,因为其获取/释放完全经由scoped_lock完成。

其特性与适用前提:

  • 不公平、不可重入(is_fair_mutex = falseis_recursive_mutex = false);
  • 当满足以下条件时,吞吐量可能优于非推测式互斥锁:
    • 运行在支持硬件事务内存的处理器上;
    • 多个线程可以并发进入互斥锁保护的临界区,且大多互不冲突;
  • 否则行为与spin_mutex类似,吞吐量甚至可能更差。

也就是说,speculative_spin_mutex适合“临界区读多写少、冲突概率低、且 CPU 支持 TSX”的场合。

3.4queuing_mutex:公平排队互斥锁

文档见 queuing_mutex_cls.rst。queuing_mutex建模 Mutex 需求,不公平的致命缺点被消除:它是公平的(fair),线程按照请求锁的先后顺序获得锁;不可重入。声明于<oneapi/tbb/queuing_mutex.h>

namespace oneapi { namespace tbb { class queuing_mutex { public: queuing_mutex() noexcept; ~queuing_mutex(); queuing_mutex(const queuing_mutex&) = delete; queuing_mutex& operator=(const queuing_mutex&) = delete; class scoped_lock; static constexpr bool is_rw_mutex = false; static constexpr bool is_recursive_mutex = false; static constexpr bool is_fair_mutex = true; }; } }

公平性通过排队(FIFO)保证:每个等待线程在队列中登记,先到先得,避免饥饿(starvation)。代价通常是比自旋锁更高的同步开销。

3.5null_mutex:空操作互斥锁

文档见 null_mutex_cls.rst。null_mutex语法层面建模 Mutex 需求,但不做任何事。它用于实例化“期望一个 Mutex 类型参数、但该实例实际上不需要互斥”的模板。声明于<oneapi/tbb/null_mutex.h>

namespace oneapi { namespace tbb { class null_mutex { public: constexpr null_mutex() noexcept; ~null_mutex(); null_mutex(const null_mutex&) = delete; null_mutex& operator=(const null_mutex&) = delete; class scoped_lock; void lock(); bool try_lock(); void unlock(); static constexpr bool is_rw_mutex = false; static constexpr bool is_recursive_mutex = true; // 恒为 true static constexpr bool is_fair_mutex = true; // 恒为 true }; } }

lock()try_lock()(恒返回true)、unlock()均为空操作。注意其 traits 与其余互斥类不同:is_recursive_mutex = trueis_fair_mutex = true——因为空锁既不冲突也永不等待,等价于“可重入且公平”。

四、读写锁(ReaderWriterMutex)类逐个详解

4.1rw_mutex:自适应读写锁

文档见 rw_mutex_cls.rst。rw_mutex以自适应方式建模 ReaderWriterMutex 需求:等待线程先自旋再阻塞。它满足 ISO C++ [thread.sharedmutex.requirements] 的全部共享互斥需求,是一个带写者偏好(writer preference)的不公平读写锁。声明于<oneapi/tbb/rw_mutex.h>

namespace oneapi { namespace tbb { class rw_mutex { public: rw_mutex() noexcept; ~rw_mutex(); rw_mutex(const rw_mutex&) = delete; rw_mutex& operator=(const rw_mutex&) = delete; class scoped_lock; // exclusive ownership void lock(); bool try_lock(); void unlock(); // shared ownership void lock_shared(); bool try_lock_shared(); void unlock_shared(); static constexpr bool is_rw_mutex = true; static constexpr bool is_recursive_mutex = false; static constexpr bool is_fair_mutex = false; }; } }

成员函数语义:

  • lock():获取写锁,自适应等待;
  • try_lock():非阻塞尝试获取锁,成功返回true,否则false
  • unlock():释放当前线程持有的写锁;
  • lock_shared():获取读锁,自适应等待;
  • try_lock_shared():非阻塞尝试获取锁,成功返回true,否则false
  • unlock_shared():释放当前线程持有的读锁。

“写者偏好”意味着一旦有写者等待,新到达的读者会被让位,从而降低写者饥饿风险。

4.2spin_rw_mutex:自旋读写锁

文档见 spin_rw_mutex_cls.rst。spin_rw_mutex建模 ReaderWriterMutex 需求,满足 ISO C++ [thread.sharedmutex.requirements],是一个带退避(backoff)与写者偏好的不公平自旋读写锁。声明于<oneapi/tbb/spin_rw_mutex.h>,接口与rw_mutex完全相同(lock/try_lock/unlock+lock_shared/try_lock_shared/unlock_shared,traits 为true/false/false)。

语义要点:

  • lock():获取写锁,若锁被占用则自旋
  • try_lock():非阻塞尝试获取写锁;
  • unlock():释放写锁;
  • lock_shared():获取读锁,若写锁已被占用则自旋
  • try_lock_shared():非阻塞尝试获取读锁;
  • unlock_shared():释放读锁。

由于是纯自旋实现,它适合“持锁时间极短、读多写少”的临界区,如保护单条计数器或标志位。

4.3speculative_spin_rw_mutex:推测式自旋读写锁

文档见 speculative_spin_rw_mutex_cls.rst。该锁建模 ReaderWriterMutex 需求,在支持硬件事务内存(如 Intel® TSX)的处理器上可实现为“允许无冲突修改并发执行”,否则行为类似spin_rw_mutex(吞吐量可能更差)。声明于<oneapi/tbb/spin_rw_mutex.h>is_fair_mutex = falseis_recursive_mutex = false

在支持 HTM 的处理器上,规范描述其可实现为如下三档语义:

  • 推测式读者与推测式写者互不阻塞(speculative readers and writers do not block each other);
  • 非推测式读者阻塞写者,但允许推测式读者继续(a non-speculative reader blocks writers but allows speculative readers);
  • 非推测式写者阻塞所有读者与写者(a non-speculative writer blocks all readers and writers)。

适用前提与speculative_spin_mutex相同:CPU 支持 HTM,且多个线程可并发进入临界区且大多无冲突。

4.4queuing_rw_mutex:公平排队读写锁

文档见 queuing_rw_mutex_cls.rst。queuing_rw_mutex建模 ReaderWriterMutex 需求,公平(按请求顺序获取锁)、不可重入。声明于<oneapi/tbb/queuing_rw_mutex.h>,类体仅声明scoped_lock与三个 traits(true/false/true),读写锁获取经由scoped_lockwrite参数区分。适合对公平性有硬性要求、且读多写少的共享数据结构。

4.5null_rw_mutex:空操作读写锁

文档见 null_rw_mutex_cls.rst。null_rw_mutex在语法层面建模 ReaderWriterMutex 需求,同样满足 ISO C++ [thread.sharedmutex.requirements] 的全部语法要求,但不做任何事,用于实例化“期望 ReaderWriterMutex 参数、但实际无需互斥”的模板。声明于<oneapi/tbb/null_rw_mutex.h>

namespace oneapi { namespace tbb { class null_rw_mutex { public: constexpr null_rw_mutex() noexcept; ~null_rw_mutex(); null_rw_mutex(const null_rw_mutex&) = delete; null_rw_mutex& operator=(const null_rw_mutex&) = delete; class scoped_lock; void lock(); bool try_lock(); void unlock(); void lock_shared(); bool try_lock_shared(); void unlock_shared(); static constexpr bool is_rw_mutex = true; static constexpr bool is_recursive_mutex = true; static constexpr bool is_fair_mutex = true; }; } }

所有操作均为空操作:try_lock()try_lock_shared()恒返回true。与null_mutex一样,三个 traits 全为true,其含义是“永远不会失败/阻塞”,而非字面意义的公平与可重入。

五、源码级验证:自适应锁到底如何实现

规范描述“自适应(adaptive)”与“先自旋再阻塞”,其真实实现可以在 mutex.h 中直接看到。TBB 将tbb::mutex定义为detail::d1::mutex(见第 88 行的using detail::d1::mutex),类体核心如下:

class mutex { public: using scoped_lock = unique_scoped_lock<mutex>; static constexpr bool is_rw_mutex = false; static constexpr bool is_recursive_mutex = false; static constexpr bool is_fair_mutex = false; void lock() { call_itt_notify(prepare, this); while (!try_lock()) { my_flag.wait(true, /* context = */ 0, std::memory_order_relaxed); } } bool try_lock() { bool result = !my_flag.load(std::memory_order_relaxed) && !my_flag.exchange(true); if (result) { call_itt_notify(acquired, this); } return result; } void unlock() { call_itt_notify(releasing, this); my_flag.exchange(false); my_flag.notify_one_relaxed(); } private: waitable_atomic<bool> my_flag{0}; };

这段实现印证了规范的多项表述:

  • 自适应自旋lock()内层循环while (!try_lock())即“自旋重试”,而my_flag.wait(true, ...)是底层waitable_atomic(可等待原子)提供的“阻塞 + 等待唤醒”原语。二者结合正是“忙等一定时间后阻塞”的自适应逻辑;
  • 非公平try_lock!my_flag.load() && !my_flag.exchange(true)抢占式获取,后到线程可直接“插队”,因此is_fair_mutex = false
  • 不可重入my_flag是一个布尔标志而非“持有者线程 ID + 计数”,同一线程重复lock()会自旋死锁,因此is_recursive_mutex = false
  • scoped_lock:由unique_scoped_lock<mutex>(位于detail/_scoped_lock.h)提供,遵循规范要求的构造获取、析构释放模式;
  • 代码中的call_itt_notify系列调用是 Intel ITT(Instrumentation and Tracing Technology)埋点,配合create_itt_sync可在 VTune 等工具中观察锁竞争,这也解释了构造函数中的create_itt_sync(this, "tbb::mutex", "")

rw_mutex的共享/独占两套接口(lock/lock_shared等)在其头文件 rw_mutex.h 中实现,接口形态与规范文档 rw_mutex_cls.rst 一一对应。

六、在 mold 中的真实应用:链接器如何使用 spin_mutex

mold 是一个现代、高速的链接器,其链接过程会启动大量 worker 线程并行处理目标文件与符号,因此互斥锁在 mold 源码中是真实存在的并发原语。从源码结构看,mold 直接复用了本仓库third-party/tbb提供的互斥类——在 mold.h 中声明了tbb::spin_mutex mu;这样的成员,用于保护需要原子性串行化的共享状态。

这正是本系列文档的典型消费场景:链接器符号合并、字符串池写入、输出段分配等临界区通常极短(几次内存读写、一条哈希表操作),持锁时间远低于一次线程上下文切换,因此选用spin_mutex(自旋而非让出 CPU)比mutex(先自旋后阻塞)或系统级pthread_mutex更贴合性能需求——这也与规范中spin_mutex“Spins if the lock is taken” 的语义完全一致。若未来 mold 需要保护“读多写少”的共享结构(如多线程并发查询、偶尔更新的映射表),规范中的rw_mutex/spin_rw_mutex(带lock_shared/try_lock_shared/unlock_shared共享所有权接口)即是对应的备选方案;若某个并行阶段实际单线程执行,则可用null_mutex作为模板参数保留接口、去除开销。

七、选型速查与使用注意事项

综合两份命名需求文档中的保证总表,可得到如下速查结论:

需求FairReentrant等待策略典型场景
MutexmutexNoNo自适应(先自旋后阻塞)通用互斥,临界区较长
Mutexspin_mutexNoNo纯自旋极短临界区,如保护计数器
Mutexspeculative_spin_mutexNoNo自旋 + HTM 推测执行冲突率低的读多写少临界区(需 TSX)
Mutexqueuing_mutexYesNoFIFO 排队需公平、防饥饿
Mutexnull_mutexYesYes空操作模板参数占位,实际不需互斥
ReaderWriterrw_mutexNoNo自适应读多写少、写者偏好
ReaderWriterspin_rw_mutexNoNo自旋 + 退避读多写少且临界区极短
ReaderWriterspeculative_spin_rw_mutexNoNo自旋 + HTM 推测执行无冲突并发读写(需 TSX)
ReaderWriterqueuing_rw_mutexYesNoFIFO 排队需公平的读写锁
ReaderWriternull_rw_mutexYesYes空操作读写锁模板参数占位

使用时的关键注意事项:

  1. 统一走scoped_lock:规范强制作用域锁模式,构造即加锁、析构即解锁,异常安全;手动lock()/unlock()需自行保证配对且无法获得异常安全;
  2. 不可拷贝、不可移动mutex与其scoped_lock均删除拷贝/移动构造与赋值,无法放入 STL 容器按值传递,通常用指针或引用间接持有;
  3. 持锁状态下销毁是未定义行为:所有类的析构函数均要求“销毁未锁定的互斥锁”;
  4. 升级/降级语义:读写锁的upgrade_to_writer()/downgrade_to_reader()返回false表示“锁曾被释放并重新获取”,调用方需据此重新校验依赖不变式;当前实现中downgrade_to_reader恒返回true
  5. 推测式锁的前提speculative_*系列仅在支持硬件事务内存(如 Intel® TSX)的处理器上才可能获得吞吐收益,且要求临界区冲突率低;否则性能可能反而更差;
  6. 否定保证可被“加强”:规范注释指出,表中标注“No”的保证,实现允许给出相反的肯定保证(更优实现),因此 trait 值以各头文件中的static constexpr为准。

如需深入阅读,规范原文与全部子文档位于 specification/source/mutual_exclusion.rst 及 mutual_exclusion/ 目录;两份命名需求详见于 named_requirements/mutexes/mutex.rst 与 named_requirements/mutexes/rw_mutex.rst;各互斥类的真实声明可对照 third-party/tbb/include/oneapi/tbb/ 下的同名头文件逐一验证。

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

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

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

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

立即咨询