尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

【C++ 面试真题】30. 聊聊 C++ 的互斥锁与读写锁

【C++ 面试真题】30. 聊聊 C++ 的互斥锁与读写锁 【C 面试真题】聊聊 C 的互斥锁与读写锁有了线程就有共享数据有了共享数据就有竞态——锁是第一道防线。背得出lock_guard 自动解锁只是及格真考你的是lock_guard 和 unique_lock 差在哪、双锁死锁怎么破、读写锁什么时候反而更慢、recursive_mutex 为什么是坏味道。本文把mutex家族一次讲透。一、开场mutex 家族有哪些❓ 介绍一下 C11 的互斥锁✅ 四种锁 三种 RAII 包装一张表锁特点场景mutex基本款不可重入绝大多数timed_mutex带超时的 lock_for/try_lock_for不能久等的路径recursive_mutex同线程可重复加锁遗留代码慎shared_mutex[C14/17]读写锁多读单写读多写少包装开销能力lock_guard最轻作用域加解锁仅此而已unique_lock稍重可延迟/手动/移动配条件变量scoped_lock[C17]轻一次锁多把无死锁回答思路先报四种锁、三种包装点一句日常 90% 的答案是 mutex lock_guard——面试官会追包装之间的差别和死锁。二、为什么必须配 RAII手挫锁的灾难❓ 直接调 lock/unlock 不行吗✅ 语法上行工程上死路——每一条提前返回、每一个异常都是一个忘了 unlock的定时炸弹mutex m;voidf(){m.lock();if(err)return;// ❌ 锁永远没了if(bad)throw0;// ❌ 同上m.unlock();// 唯一活路}lock_guard 用作用域兜底一切路径voidf(){lock_guardmutexg(m);if(err)return;// ✅ 析构自动解锁if(bad)throw0;// ✅ 异常也解锁} 这正是 RAII 思想在锁上的翻版内存管理篇的智能指针是同一招把必须成对出现的操作绑定到对象生命周期路径再多也漏不掉。三、lock_guard vs unique_lock怎么选❓ 什么时候非用 unique_lock 不可✅ lock_guard 只会出生加锁、析构解锁unique_lock 多四样本事mutex m;condition_variable cv;// ① 手动控制时机unique_lockmutexul(m);ul.unlock();// 先放do_something();ul.lock();// 再拿// ② 延迟加锁构造时先不锁unique_lockmutexul2(m,defer_lock);// 需要时再锁ul2.lock();// ③ 可移动能转移所有权// ④ 条件变量只认它cv.wait(ul);// 必须传 unique_lock代价unique_lock 内部多记当前是否持有的状态略慢于 lock_guard。选型普通临界区一律lock_guard或 C17 单锁时scoped_lock要配条件变量、要中途解锁、要转移锁所有权——才上unique_lock。四、死锁双锁互等的僵局❓ 死锁怎么发生、怎么破✅ 经典场景——两个线程反向拿两把锁// 线程 1先 A 后 Block(A);lock(B);// 线程 2先 B 后 Alock(B);lock(A);// 各拿一把、互等对方 → 永久卡死破法两招① 全局锁序——约定所有代码都按固定顺序拿锁如按地址、按 id// 永远先锁小的再锁大的voidtransfer(Accta,Acctb){autofirsta.idb.id?a:b;autoseconda.idb.id?b:a;first.m.lock();second.m.lock();// ... 转账逻辑}② std::lock / scoped_lock 一次拿全[C11/17]——用试探 回退算法避免僵持// [C17] 一步到位内部死锁免疫scoped_locklk(m1,m2);⚠️ 死锁四条件互斥/持有等待/不可剥夺/循环等待是理论框架工程上破掉循环等待一条就够——锁序统一或一把梭 scoped_lock都是干这个的。五、shared_mutex读写锁❓ 读写锁是什么什么时候用✅ 把读和写分开对待——多个读者可同时进写者独占shared_mutex rw;mapstring,intdata;// 读路径共享锁shared_lockshared_mutexr(rw);autoitdata.find(k);// 写路径独占锁unique_lockshared_mutexw(rw);data[k]v;适用前提很苛刻读操作远多于写、临界区又有一定长度配置表、元数据缓存是标准场景。⚠️读写锁不是银弹写者到达时读者要排队、读者写者来回切换有成本——读多但临界区极短时plain mutex 反而更快一次原子 CAS 就进去了读写锁的状态切换比数据本身还贵。先测再换。六、recursive_mutex能重入但别用❓ 同一个线程把一把锁锁两次会怎样✅ 普通 mutex 直接死锁——自己等自己释放。recursive_mutex 允许同线程重复加锁计数 1解锁同样要配对次数recursive_mutex m;voida(){lock_guardrecursive_mutexg(m);b();// b 里又锁 m}voidb(){lock_guardrecursive_mutexg(m);// recursive重入 OK}⚠️ 但它基本是设计坏味道需要重入说明函数职责不清公开接口和内部函数共享一把锁。正确解法是拆出不加锁的私有实现公开方法锁住后调用它——recursive_mutex 只配给改不动的遗留代码续命。// ✅ 更好的结构锁一次干活的全在里头voida(){lock_guardmutexg(m);bImpl();}voidbImpl(){/* 无锁内部版 */}七、加锁的纪律粒度与嵌套❓ 锁的粒度怎么拿捏✅ 三条纪律粒度最小只包住真正共享的操作IO、日志、耗时计算挪出临界区——锁的是数据不是整段代码嵌套最少临界区内尽量不再拿锁确需多锁走第四节的锁序/scoped_lock共享什么锁什么多把小锁各自守各自的数据分段锁优于一把大锁守全世界。// ❌ 锁住了整个业务lock_guardg(m);log();compute();save();// ✅ 只锁共享的瞬间{lock_guardg(m);xy;}log();compute();save();数据竞争的判定不是有没有锁而是同一数据是否被并发读写且至少一个是写——先理清谁共享什么锁只是最后落下的手段。八、面试高频追问❓ Q1try_lock 和 lock 的区别✅try_lock立即返回拿到为 true拿不到 false 不等待try_lock_for限时等待。适合拿不到就干别的去的路径避免阻塞、避免死锁探测。std::try_lock(m1, m2)多锁版本拿不全会全部释放返回失败。❓ Q2scoped_lock 和 lock_guard 有什么区别✅ 单锁场景几乎等价scoped_lock[C17]的本命是一次锁多把且免死锁内部用 std::lock 算法还免写模板参数。新代码多锁必 scoped_lock单锁两者随意。❓ Q3mutex 加锁的开销到底有多大✅ 无竞争时一次 lock ≈ 一次原子 CAS几十纳秒级有竞争时涉及内核等待微秒级起。所以无竞争的快路径很便宜真正贵的是竞争本身——优化方向是减少共享、缩小临界区而不是纠结锁本身。❓ Q4condition_variable 为什么必须配 unique_lock✅ wait 的机制是原子地解锁 睡眠醒来后要重新加锁——锁必须支持中途解锁再锁回lock_guard 做不到它只在析构时解锁。所以 wait 接口签名就要 unique_lock。❓ Q5spinlock自旋锁和 mutex 怎么选✅ 自旋 拿不到就空转烧 CPU用 atomic_flag 可手写下期原子篇有实现mutex 拿不到就睡眠让核。临界区极短、竞争低时自旋省了上下文切换临界区长或竞争高时自旋纯烧电。标准库 mutex 内部常见先自旋几次再睡眠的混合策略。❓ Q6锁和原子变量是什么关系✅ 都解决数据竞争。原子变量适合单个标量的无锁读写计数计数器、标志位锁适合多个变量的复合不变量转账改两个账户。原子是下期主角——粒度细、无阻塞但只守得住单个对象。九、总结速查表考点一句话结论日常答案mutex lock_guard手挫 lock异常/提前返回必漏解锁unique_lock条件变量/中途解锁/可移动scoped_lock [C17]多锁一次拿免疫死锁死锁破法全局锁序 或 scoped_lock读写锁读多写少且临界区不短recursive能重入但是设计坏味道粒度锁数据不锁过程IO 出去无竞争开销一次 CAS纳秒级mutex vs atomic复合不变量 vs 单标量一句话回顾锁的答案是 RAII日常 mutex lock_guard配条件变量或中途解锁上 unique_lock多锁交给 scoped_lock 一次拿全死锁从根上免疫读写锁只救读多写少且临界区不短recursive_mutex 是坏味道的止痛药——锁数据、不锁过程粒度就是正义。如果您觉得本篇内容对你有帮助欢迎点赞 、收藏 ⭐、转发 。下期我们继续多线程篇——聊条件变量虚假唤醒为什么必须防、丢失唤醒怎么发生的、它和 mutex 为什么是一对敬请关注
返回列表