面試陷阱題 (Exam Traps)
Purpose
全書常見錯誤、易混淆點與面試陷阱總整理。每條均可展開,並連回對應概念筆記複習。
併發基礎(第1章)
Trap: 單核 CPU 不能併發?
- 誤解:沒有多核就沒有併發。
- 單核靠任務切換仍是併發(併發假象),與真正平行只有微妙的行為差異;而多核系統上任務數常超過硬體執行緒數,任務切換與硬體併發總是並存。
- 何謂併發
Trap: 執行緒越多效能越好?
- 誤解:堆執行緒就能加速。
- 啟動開銷、堆疊消耗(32-bit + 1MB 堆疊約 4096 條耗盡位址空間)、context switch 都會使效能反降;執行緒池也非靈丹妙藥。
- 為什麼使用併發
Trap: 不呼叫 join() 只是「不等」而已?
- main 可能在新執行緒執行前就結束;而未 join/detach 的
std::thread解構會直接std::terminate()。 - C++ 併發支援與首個程式
執行緒管理(第2章)
Trap:
std::thread t(Task()); 編譯通過但執行緒沒啟動
- most vexing parse:被解析成函式宣告。
- 解法:
{}統一初始化、雙括號、命名變數或 lambda。 - 啟動與等待執行緒
Trap: 函式簽章是
T& 就會自動傳參考?
std::thread建構函式無視簽章,一律拷貝參數再以右值傳遞(非 const 參考直接編譯錯)。- 必須用
std::ref(data)包裝。 - 傳遞參數與轉移所有權
給執行緒 + detach 偶發亂碼
- 隱式轉
std::string可能晚於函式返回 → 懸空指標未定義行為。 - 傳入前先顯式
std::string(buffer)。 - 傳遞參數與轉移所有權
Trap: 直接拿 hardware_concurrency() 當執行緒數
- 可能回傳 0(資訊不可用,需預設值),且須與工作量上限取 min 避免 oversubscription。
- 執行緒數量與標識
共享資料(第3章)
Trap: 每個成員函式都上鎖了,類別就執行緒安全?
empty()/top()/pop()分離是介面固有競爭,兩個呼叫之間狀態可能改變,與實作是否上鎖無關。- 必須重設計介面,把檢查+取值+移除合併成單一操作。
- 競爭條件與互斥鎖
Trap: 「固定先鎖第一個參數」就不會死結?
swap(a,b)與swap(b,a)同時執行仍互相等待。- 解法:
std::lock(m1,m2)/std::scoped_lock同時上鎖,或全域固定順序(如鎖層級)。 - 死結與鎖管理
Trap: 沒用鎖就不會死結?
- 兩個執行緒互相
join()對方也是死結——任何等待循環都算。 - 死結與鎖管理
Trap: 雙重檢查鎖定(DCLP)又快又安全?
- 無鎖讀與上鎖寫之間無同步 = 資料競爭 UB。
- 正解:
std::call_once或 C++11 函式內static區域變數。 - 其他資料保護方式
Trap: 成員函式互相呼叫鎖不進去,換 recursive_mutex 就好?
- 第二次進入時不變量可能正處於破壞中的中間狀態,安全感是假的。
- 正解:抽出不上鎖的私有實作函式。
- 其他資料保護方式
同步操作(第4章)
Trap: wait 醒來就代表條件成立?
- 虛假喚醒:沒有 notify 也會醒。
- 必須用 predicate 版本
cv.wait(lk, pred)重查;且 predicate 不可有副作用(可能被呼叫任意次)。 - 條件變數與執行緒安全佇列
Trap: 用 lock_guard 配 cv.wait
- wait 需要在等待期間解鎖、喚醒後重新上鎖,只有
std::unique_lock支援。 - 條件變數與執行緒安全佇列
Trap: std::async 一定開新執行緒?
- 預設策略 =
async | deferred,可能延遲到 get/wait 才在呼叫端執行;從未 get 則永不執行。 - 要保證新執行緒必須顯式
std::launch::async。 - Future 與非同步任務
Trap: 用 system_clock 計算超時
- system_clock 可被校時、
now()可能倒退;超時必須用steady_clock。 - 另注意
duration_cast是截斷不是四捨五入(54802ms → 54s)。 - 限時等待
Trap: 把 latch 當 barrier 重複使用
- latch 一次性、歸零後保持就緒不重置;可重複的組同步要用 barrier(每執行緒每週期 arrive 一次)。
- 持續性、Latch 與 Barrier
記憶體模型與原子操作(第5章)
Trap: 相鄰位元欄位可用不同 mutex 各自保護?
- 它們是同一個記憶體位置,併發寫即資料競爭;須插入寬度 0 的未命名位元欄位分離。
- 記憶體模型基礎
Trap: 所有原子型別都是 lock-free?
- 標準只保證
std::atomic_flag;其餘可能內部用鎖,需is_lock_free()/is_always_lock_free檢查。 - 原子型別與操作
Trap: compare_exchange 值相等就一定成功?
weak可偽失敗(值相等仍失敗),須配迴圈。- 比較用 memcmp:padding bytes 或浮點表示差異會讓「語義相等」的值失敗。
- 原子型別與操作
Trap: acquire-release 和 seq_cst 一樣有全局順序?
- acquire-release 只成對同步;不同執行緒寫兩個變數時,兩個讀者可以看到相反的順序(經典 z==0 題目在 acq-rel 下合法)。
- 記憶體順序選項
Trap: relaxed 至少維持我程式碼的寫入順序?
- 跨變數完全可重排;只保證單一變數的修改順序不倒退。
- 記憶體順序選項
Trap: condition_variable 的 notify/wait 建立 happens-before?
- CV 本身不提供任何同步關係;可見性全靠搭配的 mutex 鎖序。
- 釋放序列與柵欄
基於鎖的資料結構(第6章)
Trap: head、tail 各配一把鎖就是細粒度佇列?
- 單元素時
head->next與tail->next是同一物件,兩把鎖護不住同一資料 → 資料競爭。 - 必須先引入 dummy node 分離 head/tail 的存取。
- 細粒度鎖佇列
Trap: notify_one 之後資料必定被取走?
- 被喚醒者可能在鎖內拋異常,資料留在佇列而其他等待者永眠。
- 解法:notify_all / 異常時補 notify_one / 佇列改存 shared_ptr(推薦)。
- 執行緒安全棧與佇列
Trap: 紅黑樹適合細粒度鎖?
- 每次操作都從根節點開始,根節點的鎖成為瓶頸;雜湊表每桶獨立上鎖才是正解。
- 查詢表與鏈結串列
Trap: 執行緒安全類別的建構/解構也安全?
- 建構與解構必然需要獨占存取;使用者須保證建構完成前、解構開始後無其他執行緒存取。
- 併發設計指南
無鎖資料結構(第7章)
Trap: 非阻塞 = 無鎖?
- 自旋鎖非阻塞但有鎖;lock-free 需要「至少一個執行緒有進展」的保證。
- 層級:obstruction-free ⊃ lock-free ⊃ wait-free。
- 無鎖概念與取捨
Trap: pop 摘下節點後立刻 delete
Trap: CAS 成功 = 期間無人動過?
- CAS 只比值不比歷史,A→B→A 照樣成功 → ABA 問題(free list/節點重用最易發生)。
- 解法:指標附帶世代計數器,對「值+計數」整體 CAS。
- 無鎖設計指導建議
Trap: 無鎖一定比有鎖快?
- 原子操作昂貴 + 快取乒乓可能拉低整體吞吐;唯一可靠的答案是實測。
- 無鎖概念與取捨
Trap: 放寬內存序後測試全過就當正確?
- 交錯組合無法窮舉,測試通過不保證內存序正確;原型先用 seq_cst,放寬需逐步論證。
- 無鎖設計指導建議 → 無鎖佇列與記憶體順序
併發設計(第8章)
Trap: 兩執行緒寫「不同變數」就不會互相拖慢?
- 同住一條 cache line 即偽共享;用
hardware_destructive_interference_size隔開。 - 區分:資料爭用 = 同一資料;偽共享 = 不同資料、同一 cache line。
- 影響性能的因素
Trap: 讀寫鎖的讀者不會造成快取乒乓?
- 讀者取得共享鎖也要修改 mutex 內部計數 → mutex 所在 cache line 照樣乒乓。
- 影響性能的因素
Trap: 把 Amdahl 當精確預測
- 它是上限估計:假設串行比例固定、忽略同步/通訊/快取開銷,實際增益通常更低。
- 異常安全與可擴展性
Trap: parallel_find 直接
future.get() 取結果
- 值不在範圍內時 promise 永不設定 → 永久等待。
- 正解:先 join 全部執行緒、檢查 done flag 再決定是否 get。
- 平行演算法案例
Trap: 工作執行緒的異常會自動傳回主執行緒?
- 無人捕捉 →
std::terminate;必須用 packaged_task/promise 存進 future,由 get() 重拋。 - 異常安全與可擴展性
進階執行緒管理(第9章)
Trap: 池內任務等待池內子任務只是變慢?
- 固定執行緒數會全數卡在等待形成死結;等待迴圈必須呼叫
run_pending_task()順手做事。 - 執行緒池
Trap:
std::function<void()> 能存任何任務?
packaged_task只可移動,std::function要求可複製 → 需自製只可移動的型別擦除包裝 (function_wrapper)。- 執行緒池
Trap: 「檢查旗標後再 wait」就能安全中斷 CV 等待?
- 檢查與進入等待之間有空窗,通知會遺失且執行緒再也叫不醒。
- 解法:
wait_for(1ms)輪詢,或condition_variable_any+ custom_lock 全程持鎖。 - 中斷執行緒
Trap: 逐條「interrupt 完就 join」關閉後台執行緒
- 被中斷執行緒要走到下一個中斷點才會結束,逐條處理會串行等待。
- 正解:先全部 interrupt 再逐一 join,讓中斷處理並行。
- 中斷執行緒
平行演算法(第10章)
Trap:
seq 行為等同不帶策略的普通重載?
seq仍是執行策略:異常逃逸照樣 terminate、複雜度放寬、元素存取順序未指定。- 執行策略
Trap: 庫沒真的開多執行緒,
par 下用普通 int 計數就沒事? Trap:
std::accumulate 加執行策略即可平行?
accumulate嚴格循序、無策略重載;平行要改用std::reduce,且運算需結合律+交換律,否則結果不確定。- 使用平行演算法
測試與除錯(第11章)
Trap: 程式卡住就是死結?
- 先看 CPU:CPU 高是活結(迴圈持續檢查),CPU 低才是死結(阻塞等待)。
- 併發 Bug 類型
Trap: 蠻力測試跑十億次都過 = 沒 bug?
- 單處理器上一切串行測不出競爭;x86 上 relaxed 與 seq_cst 載入常是同一條指令,錯誤要到弱序架構(SPARC 等)才爆發。
- 定位與測試併發 Bug
Trap: 測試程式異常時直接重拋
- 必須先
go.set_value()再重拋——否則等待 go 信號的執行緒永久阻塞,future 解構等待它們 → 測試程式自己死結。 - 定位與測試併發 Bug
附錄(A–C)
Trap:
void f(X&& x_) 內的 x_ 是右值?
- 具名右值引用參數在函式內是左值,
X a(x_)呼叫複製建構子;移動須顯式std::move(x_)。 - C++11 語言特性
Trap: 程式結束時所有 thread_local 都會解構?
- 程式退出時仍在執行的執行緒,其 thread_local 解構子不會被呼叫;且建構/解構擲例外會
std::terminate()。 - C++11 語言特性
Trap: 沒被 handle 的訊息會留在佇列等下一個狀態?
- 不匹配的訊息回溯到
dispatcher::dispatch()後直接丟棄;掛錯狀態的 handler 會讓訊息無聲消失。 - ATM 訊息傳遞範例