
并发编程高性能计算【免费下载链接】oneTBBoneAPI Threading Building Blocks (oneTBB)项目地址https://gitcode.com/gh_mirrors/on/oneTBB点击查看免费下载导读queuing_rw_mutex是 oneAPI Threading Building BlocksoneTBB提供的一种公平fair读写锁reader-writer mutex线程严格按照请求顺序获取锁且支持读锁共享、写锁独占、读锁升级与写锁降级。本文以官方参考文档 queuing_rw_mutex_cls.rst 为骨架结合 头文件 与 核心实现 的源码证据带你理解它的接口契约、公平性来源、排队算法以及如何在多线程程序中正确选用。读完你将掌握其完整 API、scoped_lock 用法、升级/降级语义并能判断它与spin_rw_mutex、rw_mutex的适用场景差异。一、什么是 queuing_rw_mutexqueuing_rw_mutex是 oneTBB 同步原语家族中的一员声明于 include/oneapi/tbb/queuing_rw_mutex.h定义在命名空间oneapi::tbb中。按官方参考文档的定义它是一个模型化ReaderWriterMutex 需求requirement概念的类也就是说它遵循 oneTBB 为读写锁定义的一套统一接口契约该契约的完整描述见 named_requirements/mutexes/rw_mutex.rst。官方文档对它的核心定性只有两点但这两点决定了它的全部行为特征非递归not recursive同一个线程不能对同一把queuing_rw_mutex重复加锁否则会死锁或触发未定义行为。公平fair线程按照请求锁的先后顺序获取锁。这是它区别于 oneTBB 中其他读写锁如spin_rw_mutex、rw_mutex最关键的属性——等待线程不会因为后来者抢占而饿死。在源码层面这些语义通过三个编译期常量traits固化在类中见 include/oneapi/tbb/queuing_rw_mutex.h// Mutex traits static constexpr bool is_rw_mutex true; // 是读写锁 static constexpr bool is_recursive_mutex false; // 不可重入 static constexpr bool is_fair_mutex true; // 公平这三个 traits 并非装饰品。oneTBB 的泛型算法与测试会通过它们静态地选择加锁策略、判断锁的性质。例如在 test/tbb/test_mutex.cpp 中queuing_rw_mutex与spin_rw_mutex、rw_mutex、null_rw_mutex等一起被纳入读写锁的通用测试套件正是依赖这些 traits 保证接口一致。二、ReaderWriterMutex 需求queuing_rw_mutex 的接口契约要真正用好queuing_rw_mutex先要理解它所满足的ReaderWriterMutex需求。该需求在 rw_mutex.rst 中定义它扩展了一般的 Mutex 需求引入一个布尔参数write来区分锁的类型write true请求写锁writer lock写锁与所有其他锁互斥持锁期间其他线程无法持有任何锁write false请求读锁reader lock只要当前没有写锁多个线程可以同时持有读锁。需求中定义的scoped_lock接口原型如下节选自 rw_mutex.rstclass 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(); }; };对照 queuing_rw_mutex.h 中scoped_lock的实际成员可以确认它完整实现了这份契约acquire(m, write)、try_acquire(m, write)、release()、upgrade_to_writer()、downgrade_to_reader()另外还额外提供一个is_writer()查询当前是否持有写锁。关于这些成员函数的语义官方需求文档给出了精确的规定成员函数语义scoped_lock()构造一个未持有任何锁的锁对象scoped_lock(RWM, bool write true)构造并获取锁write为 true 取写锁否则取读锁~scoped_lock()释放持有的锁若已持有acquire(RWM, bool write true)获取锁write决定是写锁还是读锁try_acquire(RWM, bool write true)非阻塞尝试获取锁成功返回 true失败返回 falserelease()释放锁若未持有锁则行为未定义upgrade_to_writer()将读锁升级为写锁若锁被释放后重新获取则返回 false否则返回 true已是写锁时也返回 truedowngrade_to_reader()将写锁降级为读锁若锁被释放后重新获取则返回 false否则返回 true已是读锁时也返回 true需求文档还特别提示了一个实现层面的细节对于 oneTBB 当前提供的所有读写锁downgrade_to_reader()总是返回true但需求本身并不强制其他实现必须如此。2.1 各读写锁的公平性与可重入性对比rw_mutex.rst 给出一张对照表是选择锁类型的第一手依据锁类型Fair公平Reentrant可重入rw_mutexNoNospin_rw_mutexNoNospeculative_spin_rw_mutexNoNoqueuing_rw_mutexYesNonull_rw_mutexYesYes可以看到在 oneTBB 的读写锁家族中queuing_rw_mutex是唯一“公平且不可重入”的实用读写锁null_rw_mutex虽然也标记为公平且可重入但它不提供真正的互斥语义。这张表也解释了本文标题中的“公平”二字从何而来它是需求契约层面明确保证的性质而不是实现巧合。三、类接口与源码印证queuing_rw_mutex的公开接口非常精简类声明见 include/oneapi/tbb/queuing_rw_mutex.h// Defined in header oneapi/tbb/queuing_rw_mutex.h namespace oneapi { namespace tbb { class queuing_rw_mutex { public: queuing_rw_mutex() noexcept; ~queuing_rw_mutex(); queuing_rw_mutex(const queuing_rw_mutex) delete; queuing_rw_mutex operator(const queuing_rw_mutex) delete; class scoped_lock; static constexpr bool is_rw_mutex true; static constexpr bool is_recursive_mutex false; static constexpr bool is_fair_mutex true; }; } // namespace tbb } // namespace oneapi3.1 构造与析构queuing_rw_mutex() noexcept构造一个处于未锁定状态的互斥量。在 实现源码 中构造函数同时会调用create_itt_sync为 Intel ITTInstrumentation and Tracing Technology分析工具登记同步对象方便性能剖析。~queuing_rw_mutex()析构一个未锁定的互斥量。源码中的断言include/oneapi/tbb/queuing_rw_mutex.h#L50-L52明确指出若在互斥量仍被持有内部队尾指针q_tail非空时析构会触发断言失败。这提醒我们必须在所有持锁线程退出后再销毁互斥量。拷贝与赋值两者均被 delete显式删除因为互斥量的语义不允许被复制或移动。这条规则同样适用于scoped_lock需求文档 明确要求“Mutex 类型与 M::scoped_lock 类型既不可拷贝也不可移动”。3.2 内部结构为什么 scoped_lock 是“队列节点”一个值得注意的实现事实是scoped_lock不仅仅是锁的持有凭证它本身就是公平排队算法中的队列节点。在 queuing_rw_mutex.h 的私有成员中可以看到private: //! The pointer to the mutex owned, or nullptr if not holding a mutex. queuing_rw_mutex* my_mutex; //! The pointer to the previous and next competitors for a mutex std::atomicuintptr_t my_prev; std::atomicuintptr_t my_next; //! State of the request: reader, writer, active reader, other service states std::atomicstate_t my_state; //! The local spin-wait variable std::atomicunsigned char my_going; //! A tiny internal lock std::atomicunsigned char my_internal_lock;而互斥量对象本身只保存一个原子指针q_tail队列尾部。等待线程通过scoped_lock节点组成一个隐式的等待队列这正是它公平性的物理基础——后到的线程总是排在先到者之后。四、公平性的实现原理从论文到源码源码头部的注释src/tbb/queuing_rw_mutex.cpp明确说明该实现改编自 Krieger、Stumm 等人的公平可扩展读写锁论文A Fair, Fast, Scalable Reader-Writer Lock。头文件中的类注释也写明了同样的出处include/oneapi/tbb/queuing_rw_mutex.h并标注其特性为 “local-only spinning”——即只在本地自旋等待线程自旋的是自己节点上的标志位而非共享的全局状态因此不会造成单一缓存行上的争用热点。src/tbb/queuing_rw_mutex.cpp开头的提示还强调修改此实现前必须先用 SPIN 工具模拟验证对应仓库中的tools/spin_models/ReaderWriterMutex.pml因为“有些代码看起来可以重构但它的结构至关重要”——这从侧面说明了该算法的微妙性。4.1 入队acquire 的关键路径以写锁获取为例核心流程在 acquire 中先完成scoped_lock节点所有字段的初始化设置my_mutex、清空my_prev/my_next/my_going、按write参数写入STATE_WRITER或STATE_READER状态通过m.q_tail.exchange(s, std::memory_order_acq_rel)把自己原子地交换到队尾并取回前驱节点若存在前驱则把前驱的my_next指向自己然后在自己的my_going上自旋等待——只有当真正轮到自己时前驱才会把my_going置 1 唤醒自己。这段逻辑对应源码注释src/tbb/queuing_rw_mutex.cpp#L173-L186交换操作需要 release 语义把已初始化的字段“发送”给后继者可见和 acquire 语义获取前驱的同步关系。“在本地节点上自旋”正是公平排队锁的标志性设计每个线程等待的是专属于自己的缓存行队列长度增加不会加剧缓存竞争。4.2 状态机读写锁协议的核心公平的读写锁远比互斥锁复杂因为读锁需要“共享”写锁需要“独占”。实现中用 8 个状态位描述一个锁请求所处的阶段src/tbb/queuing_rw_mutex.cpp#L88-L101状态位含义STATE_WRITER写锁持有者/请求者STATE_READER等待中的读锁请求STATE_READER_UNBLOCKNEXT读者已获锁并负责唤醒后继读者STATE_ACTIVEREADER已成为活跃读者已持有读锁STATE_UPGRADE_REQUESTED读者请求升级为写者STATE_UPGRADE_WAITING升级请求正在等待STATE_UPGRADE_LOSER升级竞争中失败锁曾被释放并重新获取读锁获取的路径比写锁复杂读者入队后会检查前驱状态。若前驱是STATE_ACTIVEREADER活跃读者说明当前已经有多读者持有锁新读者可以直接把自己置为活跃读者并进入临界区若前驱是普通STATE_READER则把前驱改为STATE_READER_UNBLOCKNEXT由前驱负责在轮到它时唤醒自己。这种“读者链”机制保证了多个读者可以在不破坏队列顺序的前提下同时持有锁同时仍保持总体公平。4.3 升级与降级upgrade_to_writer 与 downgrade_to_reader这两个操作是读写锁特有能力的实现src/tbb/queuing_rw_mutex.cpp#L419-L582downgrade_to_reader若已是读者则直接返回 true若持有写锁则把状态改为读者并尝试让后继者接管。实现结尾无条件返回 truesrc/tbb/queuing_rw_mutex.cpp#L458印证了需求文档“当前 oneTBB 读写锁的 downgrade 恒返回 true”的说明。upgrade_to_writer把状态从STATE_ACTIVEREADER依次推进到STATE_UPGRADE_REQUESTED、STATE_UPGRADE_WAITING期间可能与其他升级请求者竞争最终若变成STATE_UPGRADE_LOSER则返回 false表示“锁曾被释放并重新获取”调用方需要重新检查共享数据的稳定性否则置为STATE_WRITER并返回 true。在 test/tbb/test_mutex.cpp 中test_rwm_upgrade_downgradetbb::queuing_rw_mutex()专门覆盖了升级/降级路径的测试说明这两个操作是被官方测试保障过的稳定语义。五、使用示例scoped_lock 实战queuing_rw_mutex的使用遵循 oneTBB 的scoped locking pattern作用域锁模式。该模式的优势在 Mutex 需求文档 中有明确说明不需要手动记得释放锁且当临界区抛出异常时锁会随作用域退出自动释放不会出现死锁或锁泄漏。一个完整的读写锁使用示例#include oneapi/tbb/queuing_rw_mutex.h #include vector class ProtectedCache { std::vectorint data; // 互斥量本身不带“锁状态”状态存放在 scoped_lock 节点中 oneapi::tbb::queuing_rw_mutex mutex; public: // 写操作独占锁 void append(int value) { // 默认 write true即写锁 oneapi::tbb::queuing_rw_mutex::scoped_lock lock(mutex); data.push_back(value); // 离开作用域时自动释放写锁 } // 读操作共享锁多个读者可并发 int size() const { // write false即读锁 oneapi::tbb::queuing_rw_mutex::scoped_lock lock(mutex, false); return static_castint(data.size()); } // 先读后写读锁升级为写锁 void conditional_append(int value) { oneapi::tbb::queuing_rw_mutex::scoped_lock lock(mutex, false); if (!data.empty() data.back() value) return; // 无需写 lock.upgrade_to_writer(); // 升级为写锁 data.push_back(value); } };注意代码中的几个关键点scoped_lock lock(mutex)等价于scoped_lock lock(mutex, true)取写锁scoped_lock lock(mutex, false)取读锁多个读者线程可同时进入size()升级返回false时说明锁曾被释放并重新获取必须重新评估共享数据的当前状态不能假设临界区内的数据仍然“与升级前一致”不要用同一线程对同一把锁二次加锁因为该锁不可重入重入会死锁。另外scoped_lock还支持“延迟获取”的写法先scoped_lock lock;构造空锁稍后再lock.acquire(mutex, false)获取或在无法立即获取时用lock.try_acquire(mutex, true)非阻塞尝试。try_acquire 失败返回 false此时my_mutex不会被设置调用方应避免继续执行需要持锁的临界区代码。is_writer()可用来在持锁期间判断当前锁的类型例如在升级流程中确认自己是否已是写者。六、性能特征与选型建议6.1 何时选 queuing_rw_mutex根据官方文档与源码证据queuing_rw_mutex的适用场景可以总结为需要严格的 FIFO 公平性例如多线程读写共享数据时不希望某个写者因持续被新读者插队而饿死或在实时/批处理混合负载中要求请求顺序可预期。读多写少且读者持锁时间较长多个读者可并发持有读锁提高读吞吐。需要读→写升级或写→读降级避免“释放读锁再抢写锁”中间窗口导致的竞态与额外阻塞。它的代价是相比spin_rw_mutex基于自旋、无公平性、不适合持锁时间较长和rw_mutex自适应等待、写者优先但不公平queuing_rw_mutex的每个锁请求都需要维护队列节点与状态切换临界区应当足够短避免持锁时间过长。对于临界区极短、锁竞争激烈且能接受不公平的场景应优先考虑spin_rw_mutex等自旋锁。6.2 对比表读写锁家族的定位综合 rw_mutex.rst 与各锁的类文档spin_rw_mutexspin_rw_mutex_cls.rst不公平、不可重入纯自旋适合极短临界区、线程数不超过硬件并发度。rw_mutexrw_mutex_cls.rst自适应先自旋后阻塞、写者优先、不公平实现满足 ISO C 标准的共享互斥要求。queuing_rw_mutex本文公平、不可重入、本地自旋适合需要确定性顺序的读写场景。null_rw_mutex空实现用于“本来需要锁、但当前场景保证单线程访问”时消除开销。从 test/tbb/test_mutex.cpp 可以看出oneTBB 的测试套件将这几种读写锁统一纳入is_rw_mutex的泛型测试因此它们在接口层面可以互换选型主要依据公平性与性能特征的权衡。七、测试与 ABI 稳定性queuing_rw_mutex的正确性由多层测试保障test/tbb/test_mutex.cpp 用原生线程验证queuing_rw_mutex的互斥与读写语义同一文件中的test_rwm_upgrade_downgrade与TestIsWritertest/tbb/test_mutex.cpp#L182-L186覆盖升级/降级与锁类型查询在 test/abi/linux-64/tbb.txt 等 ABI 清单文件中queuing_rw_mutex的导出符号被登记在册用于保证跨版本的二进制兼容。八、总结queuing_rw_mutex是 oneTBB 读写锁家族中唯一兼顾公平性与实用语义的选择它以scoped_lock作为隐式排队节点通过本地自旋实现线程按请求顺序获取锁同时支持读锁共享、写锁独占以及读→写升级、写→读降级。使用时牢记三点锁不可重入、互斥量必须在无人持锁时析构、升级返回 false 时需重新检查共享状态。在需要确定性锁顺序的读多写少场景中它是比spin_rw_mutex与rw_mutex更贴合的方案。进一步阅读完整的接口契约见 ReaderWriterMutex 需求类头文件见 include/oneapi/tbb/queuing_rw_mutex.h核心算法实现见 src/tbb/queuing_rw_mutex.cpp同类可对比 rw_mutex_cls.rst 与 spin_rw_mutex_cls.rst。赞分享并发编程高性能计算【免费下载链接】oneTBBoneAPI Threading Building Blocks (oneTBB)项目地址https://gitcode.com/gh_mirrors/on/oneTBB点击查看免费下载相关推荐oneTBB 读写锁Reader Writer Mutex权威指南spin_rw_mutex 与 queuing_rw_mutex 原理与实战oneTBB 读写锁Reader Writer Mutex权威指南spin_rw_mutex 与 queuing_rw_mutex 原理与实战 导读本文并发编程高性能计算Notepad--完整指南免费跨平台文本编辑器的查找、对比与编码解决方案Notepad 完整指南免费跨平台文本编辑器的查找、对比与编码解决方案 Notepad 是一款免费开源的跨平台文本编辑器同时支持 Windows、Linux批处理流处理大数据JCSprout 源码解读ReentrantLock 实现原理AQS 公平锁与非公平锁全解析JCSprout 源码解读ReentrantLock 实现原理AQS 公平锁与非公平锁全解析 ReentrantLock 是 JDK 提供的显式可重入锁文档知识库后端教程上一篇nteract 2020/04/13 Pre-Release 变更解读配置系统 setConfigAtKey→setConfig 迁移与 CodeMirror 编辑器改造下一篇QR Code Monster与AVeryComfyNerd打造惊艳螺旋艺术的简单方法创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考