同步操作練習題 (Practice - Synchronizing Operations)
Related Concepts
- 04-Synchronizing-Operations/01-Condition-Variables-and-Thread-Safe-Queue
- 04-Synchronizing-Operations/02-Futures-and-Asynchronous-Tasks
- 04-Synchronizing-Operations/03-Waiting-with-Time-Limits
- 04-Synchronizing-Operations/04-Functional-and-Message-Passing-Styles
- 04-Synchronizing-Operations/05-Continuations-Latches-and-Barriers
| 關鍵字 | 答案 |
|---|---|
| 等待事件不耗 CPU | condition_variable::wait + predicate |
| 一次性事件 | future(條件變數非最佳) |
| 任務要回傳值 | std::async |
| 顯式設值/設例外 | std::promise |
| 任務打包入佇列/執行緒池 | std::packaged_task |
| 多執行緒等同一結果 | shared_future,每執行緒一份副本 |
| 超時計算的時鐘 | steady_clock;_for 時間段 / _until 時間點 |
| 就緒後自動接續 | .then() 持續性;future 展開 |
| 等全部 / 等任一 | when_all / when_any |
| 一次性計數 vs 可重複組同步 | latch vs barrier |
Question 1 - 條件變數與 unique_lock [recall]
為什麼
std::condition_variable::wait()必須搭配std::unique_lock而非std::lock_guard?「虛假喚醒」又是什麼、該如何應對?
等待中的執行緒必須在等待期間解鎖互斥鎖、被喚醒後重新上鎖再檢查條件;std::lock_guard 沒有這種中途解鎖/再上鎖的彈性。若休眠期間仍持鎖,生產者永遠無法上鎖推入資料。
虛假喚醒 (spurious wakeup):等待執行緒可能並非回應 notify 而醒來,次數與頻率不確定 → 必須以 predicate 重新檢查條件(wait(lk, pred)),且 predicate 不應有副作用(可能被呼叫任意多次)。
Question 2 - 啟動策略 [recall]
std::launch::async與std::launch::deferred有何差異?std::async不指定策略時的預設行為是什麼?
std::launch::async:函式必須在獨立的新執行緒上執行。std::launch::deferred:延遲到對 future 呼叫wait()/get()時,才在呼叫端執行緒上執行;若從未呼叫,函式可能永不執行。- 預設 =
deferred | async,由實作自行選擇。要保證真正併發必須明確指定std::launch::async。
Question 3 - 三種建立 future 的方式 [recall]
列出三種讓
std::future與非同步結果關聯的方式,並說明各自的適用情境。
std::async:最直接——啟動非同步任務並取回傳值。std::packaged_task<Sig>:將 future 綁定到可調用物件;任務可入佇列、交給執行緒池或指定執行緒(GUI 執行緒)執行,調度器只處理 packaged_task 而不管函式細節。std::promise<T>:最底層——顯式set_value()/set_exception();適合結果無法表達成單一函式呼叫、或來自多處的情況(如單執行緒處理多網路連接,封包 ID 對應 promise)。
Question 4 - future vs shared_future [recall]
多個執行緒要等待同一個非同步結果,直接共用一個
std::future會有什麼問題?正確做法為何?
std::future 只能移動、結果只能 get() 一次,future 物件本身不提供同步存取——多執行緒無同步地存取同一 future 會導致資料競爭與未定義行為。
正確做法:改用可拷貝的 std::shared_future(以 std::move(f)、右值隱式轉移或 f.share() 建立),並讓每個執行緒各持一份副本,各自透過自己的副本取結果即安全、無需加鎖。
Question 5 - 三種時鐘 [recall]
C++ 標準庫提供哪三種時鐘?計算超時應該用哪一個?為什麼?
system_clock:系統實際時間(牆上時鐘),可被調整 → 不穩定;可與time_t互轉。steady_clock:節拍均勻且不可調,is_steady == true。high_resolution_clock:最小節拍週期(最高精度),可能是其他時鐘的 typedef。
超時計算應使用steady_clock:system_clock可能被校時而使now()「倒退」,以它為基準的等待時間會失真。
Question 6 - latch vs barrier [recall]
比較 latch 與 barrier:可重用性、對「誰來遞減/到達」的要求,以及 flex_barrier 額外提供什麼?
- latch:一次性——計數歸 0 即就緒並保持就緒直到銷毀;不在乎誰遞減(同一執行緒可減多次、多執行緒各減一次皆可)。
- barrier:可重複使用——每週期每個執行緒只能到達一次(
arrive_and_wait),全員到齊即釋放並重置進入下一週期;arrive_and_drop可退出組(下一週期所需人數 −1)。 - flex_barrier:額外接受一個完成函式,全員到達後由其中一個執行緒執行(串行區);其回傳值可指定下一週期的執行緒數(−1 = 不變)。
Question 7 - 把例外傳給呼叫端 [application]
square_root(-1)會拋出std::out_of_range。現在要把它移到另一個執行緒執行,並讓呼叫端在取結果時看到這個例外。寫出兩種做法,並說明「promise 沒設值就銷毀」會發生什麼。
-
std::async:std::future<double> f = std::async(square_root, -1);函式拋出的例外會存入 future,呼叫f.get()時重拋(packaged_task 同理)。 -
std::promise顯式存入:try { p.set_value(square_root(x)); } catch (...) { p.set_exceptioncurrent_exception(); }若 promise/packaged_task 未設值就銷毀,future 會存入
std::future_error,錯誤碼為std::future_errc::broken_promise。
Question 8 - 限時等待迴圈 [application]
用條件變數等待
done旗標,但整體最多等 500ms。應該用wait_for還是wait_until?寫出核心迴圈並說明理由。
用 wait_until + steady_clock 的固定截止時間點:
auto const timeout = std::chrono::steady_clock::now() +
std::chrono::milliseconds(500);
std::unique_lock<std::mutex> lk(m);
while (!done)
if (cv.wait_until(lk, timeout) == std::cv_status::timeout)
break;
return done;
理由:迴圈是為了處理虛假喚醒;若用 wait_for,每次虛假喚醒後重新呼叫會重置計時,可能重複多次造成等待時間無上限。wait_until 以固定時間點為準,迴圈總長有界。
Question 9 - parallel_quick_sort 的執行緒爆炸 [analysis]
parallel_quick_sort對 lower 半部遞迴呼叫std::async。分析遞迴 10 層時的執行緒規模與效能風險、預設啟動策略下運行庫如何緩解,以及改用spawn_task(packaged_task + thread + detach)為何反而更糟。
- 規模:每層遞迴 async 一次,執行緒數約每層倍增——3 層約 8 條、10 層(約 1000 元素)可達 1024 條,遠超硬體併發數 → 任務超額、上下文切換開銷。
- 緩解:預設策略(
deferred | async)給實作自由——任務過多時可同步/延遲執行新任務(在呼叫get()的執行緒上跑,避免任務傳遞開銷);明確指定launch::async則強制每任務一執行緒。行為依實作而異。 - spawn_task 更糟:無任何自動裁剪,必定每任務開一條執行緒 → 大規模超額;其價值在於可鋪墊成「任務入佇列 + 執行緒池」的實作(第9章)。
- 另注意
std::partition仍是串行,是整體加速的天花板。
Question 10 - when_all vs async 輪詢收集 [analysis]
比較「用
std::async產生收集任務、逐一f.get()(代碼 4.22)」與「when_all+.then(代碼 4.23)」的資源使用差異。什麼情況下應改用when_any?
- async 收集版:收集任務本身佔用一條執行緒;對每個未就緒的 future 呼叫
get()會反覆「喚醒 → 未就緒 → 再休眠」,增加上下文切換與額外開銷。 when_all版:回傳的新 future 在全部就緒時才觸發.then持續性——免輪詢、不佔用執行緒;持續性內的f.get()全部立即返回。- 改用
when_any:當「任一任務完成即可繼續」時——例如多任務平行搜尋,找到任一符合值就處理;when_any_result的index指出就緒者,搭配共享std::atomic<bool>done_flag 讓其他任務提早停止;沒找到就對剩餘 futures 遞迴再掛when_any。
| 關鍵字 | 答案 |
|---|---|
| wait 需中途解鎖/再上鎖 | unique_lock(lock_guard 不行) |
| 沒 notify 也醒來 | 虛假喚醒 → predicate 重查 |
| 保證新執行緒執行 | std::launch::async |
| deferred 未 get | 永不執行 |
| async / packaged_task / promise | 取回傳值 / 打包調度 / 顯式設值 |
| 未設值銷毀 promise | broken_promise |
| 多執行緒等同一結果 | shared_future 每執行緒一副本 |
| 超時用鐘 | steady_clock;duration_cast 截斷 |
| wait_for 重置計時 | 改 wait_until 固定截止點 |
| 遞迴 async | 執行緒倍增(10 層 ≈ 1024) |
| CSP/Actor | 無共享資料、狀態機、訊息佇列 |
| 就緒自動接續 | .then();future 展開;例外沿鏈傳 |
| 等全部/任一 | when_all / when_any{futures, index} |
| 一次性計數 / 可重複組同步 | latch / barrier(flex_barrier 帶完成函式) |