C++ 递归锁(Recursive Lock)详解

发布时间:2026/7/19 17:37:11
C++ 递归锁(Recursive Lock)详解 C 递归锁Recursive Lock详解1. 什么是递归锁递归锁Recursive Mutex是指同一个线程可以多次对同一个互斥量加锁而不会导致死锁的互斥量。C 标准库中对应的类是std::recursive_mutex普通递归互斥量std::recursive_timed_mutex支持超时timed的递归互斥量2. 为什么需要递归锁与std::mutex的对比特性std::mutexstd::recursive_mutex同一线程重复加锁死锁Undefined Behavior允许内部维护计数器解锁次数要求加锁1次必须解锁1次必须解锁与加锁次数相同性能更快稍慢需要维护计数典型使用场景简单临界区递归调用、回调、同一线程多函数调用经典场景classFoo{std::mutex mtx;// 普通mutex会死锁// std::recursive_mutex mtx; // 改成这个就不会死锁public:voidfuncA(){std::lock_guardstd::mutexlock(mtx);funcB();// 在已经持有锁的情况下再次调用 funcB}voidfuncB(){std::lock_guardstd::mutexlock(mtx);// 同一线程再次加锁 → 死锁// ...}};如果把mtx换成std::recursive_mutex上面代码就能正常工作。3. 基本用法示例#includeiostream#includethread#includemutex#includechronostd::recursive_mutex rmtx;voidrecursive_func(intdepth){if(depth0)return;rmtx.lock();// 可以多次加锁std::coutDepth depth, thread id: std::this_thread::get_id()std::endl;recursive_func(depth-1);rmtx.unlock();// 必须配对解锁}intmain(){std::threadt1(recursive_func,5);std::threadt2(recursive_func,3);t1.join();t2.join();return0;}推荐写法RAIIvoidbetter_func(){std::lock_guardstd::recursive_mutexlock(rmtx);// 推荐// 或 std::unique_lockstd::recursive_mutex lock(rmtx);// 即使抛出异常也会自动解锁}4. 内部实现原理简要std::recursive_mutex通常在内部维护两个关键信息当前拥有锁的线程 IDowner_thread锁的计数器count第一次加锁owner this_threadcount 1同一线程再次加锁count解锁count--当count 0时才真正释放锁其他线程才能获取5.std::recursive_timed_mutex用法std::recursive_timed_mutex rtm;voidtimed_example(){if(rtm.try_lock_for(std::chrono::milliseconds(100))){// 获取成功rtm.unlock();}std::unique_lockstd::recursive_timed_mutexlock(rtm,std::defer_lock);if(lock.try_lock_for(std::chrono::seconds(1))){// ...}}6. 注意事项和最佳实践必须遵守的规则加锁次数必须等于解锁次数否则其他线程永远拿不到锁资源泄漏。不要在不同线程间混用同一个递归锁递归锁只对同一线程有特殊行为。尽量减少递归锁的使用它是“必要之恶”。设计建议优先使用非递归设计把大函数拆分成“加锁部分”和“不加锁部分”。使用std::lock_guard/std::unique_lock自动管理生命周期。在类中封装时可以考虑提供“已持锁”版本的内部函数classSafeClass{std::recursive_mutex mtx;void_internal_func(){// 内部已假定锁已持有// 不加锁的版本}public:voidpublic_func(){std::lock_guardstd::recursive_mutexlock(mtx);_internal_func();}};7. 性能与替代方案递归锁通常比普通std::mutex慢 10%~50%取决于平台。现代 C 中更推荐细粒度锁或无锁设计lock-free。对于需要“可重入”的场景也可以考虑std::shared_mutex 读写分离线程本地存储TLS 普通 mutex重构代码避免递归加锁总结std::recursive_mutex是为同一线程可重入场景设计的工具主要解决“已经在持有锁的情况下再次调用需要加同一把锁的函数”这个问题。使用时务必保证加锁和解锁严格配对并优先考虑能否通过代码结构优化来避免使用它。✅ 已优化代码示例结构以下是更清晰、规范、现代的代码示例结构推荐在实际项目中使用这种写法。1. 推荐的完整优化示例C17/20#includeiostream#includethread#includemutex#includevector#includechrono#includestringclassThreadSafeLogger{private:std::recursive_mutex mtx_;// 允许同一线程递归调用std::string prefix_;public:explicitThreadSafeLogger(std::string prefixLog):prefix_(std::move(prefix)){}// 公开接口支持递归调用voidlog(conststd::stringmessage,intlevel0){std::lock_guardstd::recursive_mutexlock(mtx_);std::cout[prefix_] std::string(level*2, )messagestd::endl;// 模拟递归调用同一线程多次加锁if(level2){log(message (nested),level1);}}// 批量日志演示多次加锁voidlog_batch(conststd::vectorstd::stringmessages){std::lock_guardstd::recursive_mutexlock(mtx_);for(constautomsg:messages){log(msg);// 再次进入 log()触发递归锁}}// 非递归版本内部使用假设已持有锁voidlog_internal(conststd::stringmessage){// 注意调用此函数前必须已持有 mtx_std::cout[Internal] messagestd::endl;}};intmain(){ThreadSafeLoggerlogger(Demo);// 示例1单个线程递归调用std::cout 单线程递归调用 \n;logger.log(开始任务);// 示例2多线程测试std::cout\n 多线程测试 \n;std::vectorstd::threadthreads;for(inti0;i3;i){threads.emplace_back([logger,i](){std::string msg线程 std::to_string(i) 的消息;logger.log(msg);logger.log_batch({批处理1,批处理2});});}for(autot:threads){t.join();}return0;}2. 优化点说明使用类封装更贴近实际项目而不是裸函数。RAII 完全替代全部使用std::lock_guard避免手动lock/unlock。区分公开接口与内部接口log()供外部调用加锁log_internal()内部使用假定已加锁清晰的命名mtx_后缀表示成员变量。移动语义构造函数使用std::move。可扩展性强易于后续添加try_lock、timed等功能。3. 进阶优化版本推荐避免递归锁classBetterLogger{private:std::mutex mtx_;// 使用普通 mutex更高效voidlog_internal(conststd::stringmessage,intlevel){std::cout[Log] std::string(level*2, )messagestd::endl;}public:voidlog(conststd::stringmessage,intlevel0){std::lock_guardstd::mutexlock(mtx_);log_internal(message,level);if(level2){// 通过参数传递避免递归加锁log_internal(message (nested),level1);}}};结论很多情况下通过代码结构调整把递归逻辑改为参数传递可以完全避免使用std::recursive_mutex获得更好的性能和可维护性。你希望我继续优化哪个部分A. 更复杂的实际业务场景示例如任务系统、回调系统B. 添加std::unique_lock 条件变量的完整示例C. 性能对比测试代码D. 如何安全地从递归锁迁移到普通 mutex 的技巧✅ C 无锁并发编程Lock-Free Concurrent Programming详解无锁编程是指**不使用互斥锁mutex**的情况下实现线程安全的技术。它是并发编程的高级阶段能显著提升高并发场景下的性能和可扩展性。1. 为什么需要无锁编程传统锁std::mutex存在以下问题锁竞争高线程数时性能急剧下降死锁可能出现优先级反转低优先级线程持有锁导致高优先级线程阻塞上下文切换线程被阻塞时会发生昂贵的切换无锁编程的优势无阻塞或极少阻塞更好的可扩展性线程数增加时性能下降更平缓避免死锁在某些场景下延迟更低缺点代码复杂容易出错ABA 问题、内存回收困难调试困难并非所有场景都更快低竞争时锁可能更快2. 核心概念Lock-Free至少有一个线程能持续前进系统整体不会被永久阻塞Wait-Free每个线程都能在有限步骤内完成最强保证通常最难实现Obstruction-Free单个线程在无其他线程干扰时能完成C 中主要通过std::atomic实现无锁编程。3. 基础工具std::atomic#includeatomic#includecstdintstd::atomicintcounter{0};std::atomicboolflag{false};std::atomicuint64_tshared_data{0};常用操作counter.store(42,std::memory_order_relaxed);// 写intvalcounter.load(std::memory_order_acquire);// 读// 最重要操作Compare-And-Swap (CAS)intexpected10;intdesired20;boolsuccesscounter.compare_exchange_weak(expected,desired,std::memory_order_release,std::memory_order_relaxed);4. 内存顺序Memory Order—— 性能与正确性的关键内存顺序含义性能适用场景memory_order_seq_cst顺序一致默认最慢简单场景memory_order_acquire读取时获取屏障中读取共享数据memory_order_release写入时释放屏障中写入共享数据memory_order_acq_relacquire release中RMW 操作memory_order_relaxed无同步仅原子性最快计数器、统计5. 经典无锁数据结构示例无锁递增计数器最简单classLockFreeCounter{std::atomicuint64_tcount_{0};public:voidincrement(){count_.fetch_add(1,std::memory_order_relaxed);}uint64_tget()const{returncount_.load(std::memory_order_acquire);}};无锁栈Lock-Free StacktemplatetypenameTclassLockFreeStack{structNode{T data;Node*next;Node(T val):data(std::move(val)),next(nullptr){}};std::atomicNode*head_{nullptr};public:voidpush(T value){Node*new_nodenewNode(std::move(value));Node*old_headhead_.load(std::memory_order_relaxed);do{new_node-nextold_head;}while(!head_.compare_exchange_weak(old_head,new_node,std::memory_order_release,std::memory_order_relaxed));}boolpop(Tresult){Node*old_headhead_.load(std::memory_order_acquire);while(old_head){if(head_.compare_exchange_weak(old_head,old_head-next,std::memory_order_acq_rel,std::memory_order_relaxed)){resultstd::move(old_head-data);// TODO: 内存回收hazard pointer 或 epoch-basedreturntrue;}}returnfalse;}};6. 常见难题与解决方案ABA 问题线程1 看到 A→B→A误以为值没变。解决使用带版本号的指针tagged pointer或std::atomicstd::shared_ptrC20 较慢。内存回收不能直接delete因为其他线程可能还在使用。常用方案Hazard PointersEpoch-Based Reclamation (RCU 风格)SMR (Safe Memory Reclamation)无锁队列Michael Scott 队列最经典7. C20 后的新特性支持std::atomicstd::shared_ptrT简化但性能一般std::atomic_refTC20对已有对象提供原子操作std::counting_semaphore、std::barrier等8. 实用建议低竞争场景→ 用std::mutex更简单高效高竞争、热点路径→ 考虑无锁或无等待算法先用std::atomic实现简单统计、标志位不要盲目追求无锁先衡量性能瓶颈用perf/ VTune推荐库Boost.LockfreeFollyFacebookTBBIntel Threading Building Blocksmoodycamel::ConcurrentQueue极受欢迎的无锁队列想深入学习哪个部分完整无锁队列实现 ABA 解决Hazard Pointers 详细实现性能对比测试mutex vs atomic vs lockfree实际项目中的无锁模式如内存池、日志系统C20/23 无锁新特性