C++ 递归锁(Recursive Lock)详解
1. 什么是递归锁?
递归锁(Recursive Mutex)是指同一个线程可以多次对同一个互斥量加锁而不会导致死锁的互斥量。
C++ 标准库中对应的类是:
std::recursive_mutex:普通递归互斥量std::recursive_timed_mutex:支持超时(timed)的递归互斥量
2. 为什么需要递归锁?(与std::mutex的对比)
| 特性 | std::mutex | std::recursive_mutex |
|---|---|---|
| 同一线程重复加锁 | 死锁(Undefined Behavior) | 允许,内部维护计数器 |
| 解锁次数要求 | 加锁1次必须解锁1次 | 必须解锁与加锁次数相同 |
| 性能 | 更快 | 稍慢(需要维护计数) |
| 典型使用场景 | 简单临界区 | 递归调用、回调、同一线程多函数调用 |
经典场景:
classFoo{std::mutex mtx;// 普通mutex会死锁// std::recursive_mutex mtx; // 改成这个就不会死锁public:voidfuncA(){std::lock_guard<std::mutex>lock(mtx);funcB();// 在已经持有锁的情况下再次调用 funcB}voidfuncB(){std::lock_guard<std::mutex>lock(mtx);// 同一线程再次加锁 → 死锁!// ...}};如果把mtx换成std::recursive_mutex,上面代码就能正常工作。
3. 基本用法示例
#include<iostream>#include<thread>#include<mutex>#include<chrono>std::recursive_mutex rmtx;voidrecursive_func(intdepth){if(depth<=0)return;rmtx.lock();// 可以多次加锁std::cout<<"Depth "<<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;}推荐写法(RAII):
voidbetter_func(){std::lock_guard<std::recursive_mutex>lock(rmtx);// 推荐// 或 std::unique_lock<std::recursive_mutex> lock(rmtx);// 即使抛出异常也会自动解锁}4. 内部实现原理(简要)
std::recursive_mutex通常在内部维护两个关键信息:
- 当前拥有锁的线程 ID(
owner_thread) - 锁的计数器(
count)
- 第一次加锁:
owner = this_thread,count = 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_lock<std::recursive_timed_mutex>lock(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_guard<std::recursive_mutex>lock(mtx);_internal_func();}};7. 性能与替代方案
- 递归锁通常比普通
std::mutex慢 10%~50%(取决于平台)。 - 现代 C++ 中,更推荐细粒度锁或无锁设计(lock-free)。
- 对于需要“可重入”的场景,也可以考虑:
std::shared_mutex+ 读写分离- 线程本地存储(TLS)+ 普通 mutex
- 重构代码避免递归加锁
总结:
std::recursive_mutex是为同一线程可重入场景设计的工具,主要解决“已经在持有锁的情况下再次调用需要加同一把锁的函数”这个问题。使用时务必保证加锁和解锁严格配对,并优先考虑能否通过代码结构优化来避免使用它。
✅ 已优化代码示例结构
以下是更清晰、规范、现代的代码示例结构,推荐在实际项目中使用这种写法。
1. 推荐的完整优化示例(C++17/20)
#include<iostream>#include<thread>#include<mutex>#include<vector>#include<chrono>#include<string>classThreadSafeLogger{private:std::recursive_mutex mtx_;// 允许同一线程递归调用std::string prefix_;public:explicitThreadSafeLogger(std::string prefix="Log"):prefix_(std::move(prefix)){}// 公开接口:支持递归调用voidlog(conststd::string&message,intlevel=0){std::lock_guard<std::recursive_mutex>lock(mtx_);std::cout<<"["<<prefix_<<"] "<<std::string(level*2,' ')<<message<<std::endl;// 模拟递归调用(同一线程多次加锁)if(level<2){log(message+" (nested)",level+1);}}// 批量日志(演示多次加锁)voidlog_batch(conststd::vector<std::string>&messages){std::lock_guard<std::recursive_mutex>lock(mtx_);for(constauto&msg:messages){log(msg);// 再次进入 log(),触发递归锁}}// 非递归版本(内部使用,假设已持有锁)voidlog_internal(conststd::string&message){// 注意:调用此函数前必须已持有 mtx_!std::cout<<"[Internal] "<<message<<std::endl;}};intmain(){ThreadSafeLoggerlogger("Demo");// 示例1:单个线程递归调用std::cout<<"=== 单线程递归调用 ===\n";logger.log("开始任务");// 示例2:多线程测试std::cout<<"\n=== 多线程测试 ===\n";std::vector<std::thread>threads;for(inti=0;i<3;++i){threads.emplace_back([&logger,i](){std::string msg="线程 "+std::to_string(i)+" 的消息";logger.log(msg);logger.log_batch({"批处理1","批处理2"});});}for(auto&t: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::string&message,intlevel){std::cout<<"[Log] "<<std::string(level*2,' ')<<message<<std::endl;}public:voidlog(conststd::string&message,intlevel=0){std::lock_guard<std::mutex>lock(mtx_);log_internal(message,level);if(level<2){// 通过参数传递,避免递归加锁log_internal(message+" (nested)",level+1);}}};结论:很多情况下,通过代码结构调整(把递归逻辑改为参数传递),可以完全避免使用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
#include<atomic>#include<cstdint>std::atomic<int>counter{0};std::atomic<bool>flag{false};std::atomic<uint64_t>shared_data{0};常用操作:
counter.store(42,std::memory_order_relaxed);// 写intval=counter.load(std::memory_order_acquire);// 读// 最重要操作:Compare-And-Swap (CAS)intexpected=10;intdesired=20;boolsuccess=counter.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_rel | acquire + release | 中 | RMW 操作 |
memory_order_relaxed | 无同步,仅原子性 | 最快 | 计数器、统计 |
5. 经典无锁数据结构示例
无锁递增计数器(最简单)
classLockFreeCounter{std::atomic<uint64_t>count_{0};public:voidincrement(){count_.fetch_add(1,std::memory_order_relaxed);}uint64_tget()const{returncount_.load(std::memory_order_acquire);}};无锁栈(Lock-Free Stack)
template<typenameT>classLockFreeStack{structNode{T data;Node*next;Node(T val):data(std::move(val)),next(nullptr){}};std::atomic<Node*>head_{nullptr};public:voidpush(T value){Node*new_node=newNode(std::move(value));Node*old_head=head_.load(std::memory_order_relaxed);do{new_node->next=old_head;}while(!head_.compare_exchange_weak(old_head,new_node,std::memory_order_release,std::memory_order_relaxed));}boolpop(T&result){Node*old_head=head_.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)){result=std::move(old_head->data);// TODO: 内存回收(hazard pointer 或 epoch-based)returntrue;}}returnfalse;}};6. 常见难题与解决方案
ABA 问题
线程1 看到 A→B→A,误以为值没变。
解决:使用带版本号的指针(tagged pointer)或std::atomic<std::shared_ptr>(C++20 较慢)。内存回收
不能直接delete,因为其他线程可能还在使用。
常用方案:- Hazard Pointers
- Epoch-Based Reclamation (RCU 风格)
- SMR (Safe Memory Reclamation)
无锁队列:Michael & Scott 队列(最经典)
7. C++20 后的新特性支持
std::atomic<std::shared_ptr<T>>(简化但性能一般)std::atomic_ref<T>(C++20,对已有对象提供原子操作)std::counting_semaphore、std::barrier等
8. 实用建议
- 低竞争场景→ 用
std::mutex更简单高效 - 高竞争、热点路径→ 考虑无锁或无等待算法
- 先用
std::atomic实现简单统计、标志位 - 不要盲目追求无锁,先衡量性能瓶颈(用
perf/ VTune) - 推荐库:
- Boost.Lockfree
- Folly(Facebook)
- TBB(Intel Threading Building Blocks)
- moodycamel::ConcurrentQueue(极受欢迎的无锁队列)
想深入学习哪个部分?
- 完整无锁队列实现 + ABA 解决
- Hazard Pointers 详细实现
- 性能对比测试(mutex vs atomic vs lockfree)
- 实际项目中的无锁模式(如内存池、日志系统)
- C++20/23 无锁新特性