C++ STL之互斥锁与条件变量详解

C++ STL之互斥锁与条件变量详解 C STL之互斥锁与条件变量详解一、从数据竞争说起多线程同时读写同一内存结果不确定这就是数据竞争。C 标准库提供了mutex系列锁和condition_variable来解决线程同步问题。从 C11 到 C17同步原语逐步完善每个都有明确的适用场景。二、互斥锁四兄弟std::mutex – 基础互斥锁最基础的排他锁。lock()阻塞直到获得锁unlock()释放。不可递归——同一线程二次调用lock()产生未定义行为。适合临界区极短的场景。std::recursive_mutex – 可重入锁同一线程允许重复加锁内部维护计数调用几次lock()就要调用几次unlock()。适用于递归函数中需要加锁的场景但通常意味着设计可优化——多数时候可以把锁提到递归外。std::timed_mutex – 超时锁在mutex基础上增加了try_lock_for(duration)和try_lock_until(time_point)超时返回false而非死等。适合不能无限阻塞的 I/O 或网络操作。std::shared_mutexC17– 读写锁这是最常用的高性能锁。读操作可以共享锁写操作必须独占。C14 的shared_timed_mutex增加了超时版本。读操作多个线程写操作单一线程shared_mutex 读写锁当前访问类型?lock_shared()共享锁lock()独占锁多个读者并行无竞争写者等待所有读者释放后进入最后一个读者解锁写者可进入写者解锁读者/写者竞争读写锁对比一览特性mutexrecursive_mutextimed_mutexshared_mutex排他性独占独占独占读共享/写独占可重入否是否否超时否否是是C14性能最快略慢同mutex读多写少最优三、RAII 锁包装器手动lock/unlock容易遗漏异常安全路径RAII 包装器让锁随作用域自动释放。lock_guard – 最简单的 RAII构造时加锁析构时解锁。不可复制不可移动不可手动解锁。适用简单临界区std::mutex mtx;{std::lock_guardstd::mutexlock(mtx);// 临界区自动管理}unique_lock – 灵活的 RAII比lock_guard多了三个能力延迟加锁构造时不锁稍后lock()、提前解锁unlock()减少持有时间、转移所有权move语义。常配合condition_variablestd::mutex mtx;std::unique_lockstd::mutexlock(mtx,std::defer_lock);// 不立即锁// ... 其他操作 ...lock.lock();// 需要时再加scoped_lockC17– 防死锁的多锁方案一次锁多个 mutex内部使用std::lock的死锁避免算法按固定顺序尝试加锁是同时加锁多个互斥量的首选std::mutex m1,m2;{std::scoped_locklock(m1,m2);// 同时锁住死锁安全// 操作两个临界区}C17 之前只能用std::lock(m1, m2)lock_guard配合scoped_lock把这个模式包装成了一行。四、条件变量与虚假唤醒std::condition_variable配合unique_lock使用线程可以等待某个条件成立再继续。std::mutex mtx;std::condition_variable cv;boolreadyfalse;// 等待线程std::unique_lockstd::mutexlock(mtx);cv.wait(lock,[]{returnready;});// 等效于 while(!ready) cv.wait(lock);// 通知线程{std::lock_guardstd::mutexlock(mtx);readytrue;}cv.notify_one();虚假唤醒spurious wakeup是条件变量的固有问题——wait可能在未被通知时返回。操作系统行为、信号处理等都可能导致。必须用 while 循环二次检查谓词不能假设醒来就是条件满足。条件为真条件为假线程调用cv.wait(lock)检查谓词条件(while 循环)跳过等待继续执行线程阻塞释放 mutex收到通知或被虚假唤醒持有 mutex执行临界区带谓词的wait(lock, predicate)等价于while (!predicate()) { wait(lock); }是 C 标准的推荐写法。永远不要用无谓词的wait。五、死锁四条件死锁必须同时满足四个条件互斥——资源一次只能被一个线程占用持有并等待——线程持有一个资源同时等待另一个不可剥夺——资源只能由持有者主动释放循环等待——存在线程间环形等待链工程对策固定加锁顺序——所有线程按相同顺序加锁先 A 后 Bstd::lock/scoped_lock——一次锁多个内部避免死锁try_lock回退——加锁失败时释放已持有的锁死锁最难排查——不崩溃、不报错程序卡死。生产环境常配合std::lock_guard 严格代码审查来预防。六、面试题Q1lock_guard和unique_lock的区别lock_guard不可解锁不可转移极简 RAII。unique_lock可解锁、可转移、可延迟加锁但多了虚函数调用性能略低约 5%~10%。需要配合condition_variable时必须用unique_lock。Q2什么是虚假唤醒如何避免操作系统可能在无通知时唤醒wait返回。必须用带谓词的wait(lock, pred)或while (!pred) wait(lock)二次检查。Q3shared_mutex适用于什么场景读远多于写如配置表、缓存读操作可并发写操作排他。典型如 DNS 缓存、配置中心。Q4scoped_lock如何避免死锁内部调用std::lock(...)使用算法如Try-Lock 排序或回退策略确保多个锁的加锁操作整体是原子且无环的。C17 起应优先使用。Q5recursive_mutex有什么问题掩盖了代码结构问题——递归加锁常意味着加锁粒度过大或职责不清。大多数场景应拆分子函数让每个函数只锁需要的部分。Q6如果mutex.lock()抛出异常怎么办mutex.lock()本身不抛异常无noexcept但在实践中不会因业务逻辑抛异常。更常见的是临界区代码抛异常——此时必须用 RAII 包装器确保unlock否则 mutex 被永远锁住。Q7try_lock_for的典型用法std::timed_mutex mtx;if(mtx.try_lock_for(std::chrono::milliseconds(100))){// 获得锁mtx.unlock();}else{// 超时处理}七、总结锁一句话适用场景mutex短临界区不需要重入/超时recursive_mutex递归函数必须加锁尽量重构timed_mutex有超时容错需求shared_mutex读极多写极少scoped_lock多锁同时加防死锁首选C 同步原语链从mutex到shared_mutexRAII 包装器从lock_guard到scoped_lock逐层解决更复杂的并发问题。记住三点RAII 保异常安全、while 防虚假唤醒、固定顺序防死锁。