
【C 面試真題】聊聊 C 的互斥鎖與讀寫鎖有了線程就有共享數(shù)據(jù)有了共享數(shù)據(jù)就有競態(tài)——鎖是第一道防線。背得出lock_guard 自動解鎖只是及格真考你的是lock_guard 和 unique_lock 差在哪、雙鎖死鎖怎么破、讀寫鎖什么時候反而更慢、recursive_mutex 為什么是壞味道。本文把mutex家族一次講透。一、開場mutex 家族有哪些? 介紹一下 C11 的互斥鎖? 四種鎖 三種 RAII 包裝一張表鎖特點場景mutex基本款不可重入絕大多數(shù)timed_mutex帶超時的 lock_for/try_lock_for不能久等的路徑recursive_mutex同線程可重復(fù)加鎖遺留代碼慎shared_mutex[C14/17]讀寫鎖多讀單寫讀多寫少包裝開銷能力lock_guard最輕作用域加解鎖僅此而已unique_lock稍重可延遲/手動/移動配條件變量scoped_lock[C17]輕一次鎖多把無死鎖回答思路先報四種鎖、三種包裝點一句日常 90% 的答案是 mutex lock_guard——面試官會追包裝之間的差別和死鎖。二、為什么必須配 RAII手挫鎖的災(zāi)難? 直接調(diào) lock/unlock 不行嗎? 語法上行工程上死路——每一條提前返回、每一個異常都是一個忘了 unlock的定時炸彈mutex m;voidf(){m.lock();if(err)return;// ? 鎖永遠(yuǎn)沒了if(bad)throw0;// ? 同上m.unlock();// 唯一活路}lock_guard 用作用域兜底一切路徑voidf(){lock_guardmutexg(m);if(err)return;// ? 析構(gòu)自動解鎖if(bad)throw0;// ? 異常也解鎖} 這正是 RAII 思想在鎖上的翻版內(nèi)存管理篇的智能指針是同一招把必須成對出現(xiàn)的操作綁定到對象生命周期路徑再多也漏不掉。三、lock_guard vs unique_lock怎么選? 什么時候非用 unique_lock 不可? lock_guard 只會出生加鎖、析構(gòu)解鎖unique_lock 多四樣本事mutex m;condition_variable cv;// ① 手動控制時機(jī)unique_lockmutexul(m);ul.unlock();// 先放do_something();ul.lock();// 再拿// ② 延遲加鎖構(gòu)造時先不鎖unique_lockmutexul2(m,defer_lock);// 需要時再鎖ul2.lock();// ③ 可移動能轉(zhuǎn)移所有權(quán)// ④ 條件變量只認(rèn)它cv.wait(ul);// 必須傳 unique_lock代價unique_lock 內(nèi)部多記當(dāng)前是否持有的狀態(tài)略慢于 lock_guard。選型普通臨界區(qū)一律lock_guard或 C17 單鎖時scoped_lock要配條件變量、要中途解鎖、要轉(zhuǎn)移鎖所有權(quán)——才上unique_lock。四、死鎖雙鎖互等的僵局? 死鎖怎么發(fā)生、怎么破? 經(jīng)典場景——兩個線程反向拿兩把鎖// 線程 1先 A 后 Block(A);lock(B);// 線程 2先 B 后 Alock(B);lock(A);// 各拿一把、互等對方 → 永久卡死破法兩招① 全局鎖序——約定所有代碼都按固定順序拿鎖如按地址、按 id// 永遠(yuǎn)先鎖小的再鎖大的voidtransfer(Accta,Acctb){autofirsta.idb.id?a:b;autoseconda.idb.id?b:a;first.m.lock();second.m.lock();// ... 轉(zhuǎn)賬邏輯}② std::lock / scoped_lock 一次拿全[C11/17]——用試探 回退算法避免僵持// [C17] 一步到位內(nèi)部死鎖免疫scoped_locklk(m1,m2);?? 死鎖四條件互斥/持有等待/不可剝奪/循環(huán)等待是理論框架工程上破掉循環(huán)等待一條就夠——鎖序統(tǒng)一或一把梭 scoped_lock都是干這個的。五、shared_mutex讀寫鎖? 讀寫鎖是什么什么時候用? 把讀和寫分開對待——多個讀者可同時進(jìn)寫者獨占shared_mutex rw;mapstring,intdata;// 讀路徑共享鎖shared_lockshared_mutexr(rw);autoitdata.find(k);// 寫路徑獨占鎖unique_lockshared_mutexw(rw);data[k]v;適用前提很苛刻讀操作遠(yuǎn)多于寫、臨界區(qū)又有一定長度配置表、元數(shù)據(jù)緩存是標(biāo)準(zhǔn)場景。??讀寫鎖不是銀彈寫者到達(dá)時讀者要排隊、讀者寫者來回切換有成本——讀多但臨界區(qū)極短時plain mutex 反而更快一次原子 CAS 就進(jìn)去了讀寫鎖的狀態(tài)切換比數(shù)據(jù)本身還貴。先測再換。六、recursive_mutex能重入但別用? 同一個線程把一把鎖鎖兩次會怎樣? 普通 mutex 直接死鎖——自己等自己釋放。recursive_mutex 允許同線程重復(fù)加鎖計數(shù) 1解鎖同樣要配對次數(shù)recursive_mutex m;voida(){lock_guardrecursive_mutexg(m);b();// b 里又鎖 m}voidb(){lock_guardrecursive_mutexg(m);// recursive重入 OK}?? 但它基本是設(shè)計壞味道需要重入說明函數(shù)職責(zé)不清公開接口和內(nèi)部函數(shù)共享一把鎖。正確解法是拆出不加鎖的私有實現(xiàn)公開方法鎖住后調(diào)用它——recursive_mutex 只配給改不動的遺留代碼續(xù)命。// ? 更好的結(jié)構(gòu)鎖一次干活的全在里頭voida(){lock_guardmutexg(m);bImpl();}voidbImpl(){/* 無鎖內(nèi)部版 */}七、加鎖的紀(jì)律粒度與嵌套? 鎖的粒度怎么拿捏? 三條紀(jì)律粒度最小只包住真正共享的操作IO、日志、耗時計算挪出臨界區(qū)——鎖的是數(shù)據(jù)不是整段代碼嵌套最少臨界區(qū)內(nèi)盡量不再拿鎖確需多鎖走第四節(jié)的鎖序/scoped_lock共享什么鎖什么多把小鎖各自守各自的數(shù)據(jù)分段鎖優(yōu)于一把大鎖守全世界。// ? 鎖住了整個業(yè)務(wù)lock_guardg(m);log();compute();save();// ? 只鎖共享的瞬間{lock_guardg(m);xy;}log();compute();save();數(shù)據(jù)競爭的判定不是有沒有鎖而是同一數(shù)據(jù)是否被并發(fā)讀寫且至少一個是寫——先理清誰共享什么鎖只是最后落下的手段。八、面試高頻追問? Q1try_lock 和 lock 的區(qū)別?try_lock立即返回拿到為 true拿不到 false 不等待try_lock_for限時等待。適合拿不到就干別的去的路徑避免阻塞、避免死鎖探測。std::try_lock(m1, m2)多鎖版本拿不全會全部釋放返回失敗。? Q2scoped_lock 和 lock_guard 有什么區(qū)別? 單鎖場景幾乎等價scoped_lock[C17]的本命是一次鎖多把且免死鎖內(nèi)部用 std::lock 算法還免寫模板參數(shù)。新代碼多鎖必 scoped_lock單鎖兩者隨意。? Q3mutex 加鎖的開銷到底有多大? 無競爭時一次 lock ≈ 一次原子 CAS幾十納秒級有競爭時涉及內(nèi)核等待微秒級起。所以無競爭的快路徑很便宜真正貴的是競爭本身——優(yōu)化方向是減少共享、縮小臨界區(qū)而不是糾結(jié)鎖本身。? Q4condition_variable 為什么必須配 unique_lock? wait 的機(jī)制是原子地解鎖 睡眠醒來后要重新加鎖——鎖必須支持中途解鎖再鎖回lock_guard 做不到它只在析構(gòu)時解鎖。所以 wait 接口簽名就要 unique_lock。? Q5spinlock自旋鎖和 mutex 怎么選? 自旋 拿不到就空轉(zhuǎn)燒 CPU用 atomic_flag 可手寫下期原子篇有實現(xiàn)mutex 拿不到就睡眠讓核。臨界區(qū)極短、競爭低時自旋省了上下文切換臨界區(qū)長或競爭高時自旋純燒電。標(biāo)準(zhǔn)庫 mutex 內(nèi)部常見先自旋幾次再睡眠的混合策略。? Q6鎖和原子變量是什么關(guān)系? 都解決數(shù)據(jù)競爭。原子變量適合單個標(biāo)量的無鎖讀寫計數(shù)計數(shù)器、標(biāo)志位鎖適合多個變量的復(fù)合不變量轉(zhuǎn)賬改兩個賬戶。原子是下期主角——粒度細(xì)、無阻塞但只守得住單個對象。九、總結(jié)速查表考點一句話結(jié)論日常答案mutex lock_guard手挫 lock異常/提前返回必漏解鎖unique_lock條件變量/中途解鎖/可移動scoped_lock [C17]多鎖一次拿免疫死鎖死鎖破法全局鎖序 或 scoped_lock讀寫鎖讀多寫少且臨界區(qū)不短recursive能重入但是設(shè)計壞味道粒度鎖數(shù)據(jù)不鎖過程IO 出去無競爭開銷一次 CAS納秒級mutex vs atomic復(fù)合不變量 vs 單標(biāo)量一句話回顧鎖的答案是 RAII日常 mutex lock_guard配條件變量或中途解鎖上 unique_lock多鎖交給 scoped_lock 一次拿全死鎖從根上免疫讀寫鎖只救讀多寫少且臨界區(qū)不短recursive_mutex 是壞味道的止痛藥——鎖數(shù)據(jù)、不鎖過程粒度就是正義。如果您覺得本篇內(nèi)容對你有幫助歡迎點贊 、收藏 ?、轉(zhuǎn)發(fā) 。下期我們繼續(xù)多線程篇——聊條件變量虛假喚醒為什么必須防、丟失喚醒怎么發(fā)生的、它和 mutex 為什么是一對敬請關(guān)注