進階執行緒管理練習題 (Practice - Advanced Thread Management)
Related Concepts
- 09-Advanced-Thread-Management/01-Thread-Pools
- 09-Advanced-Thread-Management/02-Work-Stealing-and-Queue-Contention
- 09-Advanced-Thread-Management/03-Interrupting-Threads
| 關鍵字 | 答案 |
|---|---|
| worker 取不到任務 | std::this_thread::yield() 讓出 CPU |
packaged_task 進不了 std::function |
只可移動 vs 要求可複製 → function_wrapper(型別擦除) |
| 池內任務等待池內任務 | 死結 (deadlock) → 等待迴圈呼叫 run_pending_task() |
| 單一全域佇列變慢 | 爭用 + 快取乒乓 → thread_local 本地佇列 |
| 本地佇列滿、他人閒置 | 工作竊取 (work stealing):擁有者前端 LIFO、竊取者後端 FIFO |
| 取任務三層順序 | 本地 → 全域 → 竊取他人,失敗則 yield |
| 中斷檢查 | interruption_point() 檢查 thread_local 旗標 → 拋 thread_interrupted |
中斷 cv.wait() 的競爭 |
檢查與等待間通知遺失 → wait_for(1ms) 輪詢或 condition_variable_any + custom_lock |
| 多條後台執行緒退出 | 先全部 interrupt(),再逐一 join() |
Question 1 - 簡單執行緒池的 worker 迴圈 [recall]
代碼 9.1 的
worker_thread()主迴圈做了哪些事?取不到任務時為什麼要呼叫std::this_thread::yield()?
迴圈在 !done 期間反覆:work_queue.try_pop(task) 成功就執行 task(),失敗就 std::this_thread::yield()。
yield 讓出 CPU 時間片,給其他執行緒(包括要 submit() 推任務的執行緒)執行機會,避免空轉迴圈霸佔處理器。
解構時只需設 done=true,join_threads (RAII) 保證所有 worker 匯入後池才銷毀。
Question 2 - 為什麼需要 function_wrapper [recall]
可等待任務的執行緒池(代碼 9.2)為何不能繼續用
std::function<void()>作任務佇列的元素型別?
等待任務要靠 std::packaged_task<> 產生 future,但 packaged_task 只可移動、不可複製;而 std::function<> 要求存放可複製建構的可呼叫物件,兩者衝突。
解法是自製 function_wrapper:以 impl_base 虛擬函式 + impl_type<F> 做型別擦除 (type erasure),只提供移動建構/移動指派,複製一律 =delete。
Question 3 - submit() 如何回傳 future [recall]
描述代碼 9.2 中
submit()從收到可呼叫物件到回傳std::future<>的完整流程。
(1) 用 std::result_of<FunctionTypetype 推導回傳型別;(2) 把 f 包進 std::packaged_task<result_type()>;(3) 先呼叫 task.get_future() 取得 future;(4) 以 std::move(task) 推入佇列(不可複製);(5 回傳 future。
任務中拋出的異常會存入 future,呼叫端 get() 時重新拋出。
Question 4 - thread_local 本地任務佇列 [recall]
代碼 9.6 中本地佇列宣告為
static thread_local std::unique_ptr<std::queue<function_wrapper>>,為何本地佇列可用普通std::queue?為何要用unique_ptr?
普通 std::queue 即可:本地佇列只有擁有它的執行緒會存取,無競爭故不需鎖 —— 這正是消除全域佇列爭用與快取乒乓的關鍵。
用 unique_ptr 是因為非池內執行緒不該擁有本地佇列:指標為空即代表外部執行緒,submit() 據此把任務改推入全域佇列;worker 在主迴圈前 reset() 初始化,執行緒退出時自動銷毀。
Question 5 - 工作竊取佇列的前端與後端 [recall]
代碼 9.7 的
work_stealing_queue中,擁有者的push()/try_pop()與竊取者的try_steal()分別操作 deque 的哪一端?為什麼這樣設計?
擁有者操作前端(push_front/pop_front,LIFO);竊取者 try_steal() 操作後端(pop_back,FIFO)。
擁有者 LIFO 有快取親和性:最新任務的資料還熱在快取,對快速排序等遞迴演算法也能減少活動任務數;竊取者取後端則減少與 try_pop() 的競爭(兩者操作不同端)。
書中以 mutex 保護 deque 即可,因為竊取預期是少見事件。
Question 6 - interruption_point() 的行為 [recall]
interruption_point()做什麼事?為什麼光靠它不足以中斷所有執行緒?
檢查當前執行緒的 thread_local interrupt_flag,若已設定就拋出 thread_interrupted 異常 —— 屬於協作式中斷,需在程式碼安全處顯式呼叫。
不足之處:執行緒阻塞(如卡在 cv.wait() 或等待 future)時根本不在執行,無法呼叫此函式檢查旗標,因此需要 interruptible_wait() 系列函式。
Question 7 - 池上快速排序死結 [application]
你把平行快速排序改到固定大小的執行緒池上:每層遞迴
submit()一半資料再等待 future,結果所有 worker 卡死。診斷原因並給出修法。
原因:池只有固定數量執行緒,當所有 worker 都在等待子任務的 future、而那些子任務還躺在佇列裡沒有執行緒能執行時,形成死結 (deadlock)。
修法:等待迴圈中呼叫 run_pending_task() —— while (new_lower.wait_for(0s) == std::future_status::timeout) pool.run_pending_task();,讓等待中的執行緒也幫忙消化佇列上的任務,等待與做事同時進行。
Question 8 - 應用退出時關閉後台執行緒 [application]
桌面應用有多條
interruptible_thread後台執行緒監視檔案系統。使用者按下退出,你會怎麼有序地關閉它們?為何不「中斷一條就 join 一條」?
先對所有後台執行緒呼叫 interrupt(),再逐一 join()(代碼 9.13)。
被中斷的執行緒不會立即結束 —— 要走到下一個 interruption_point()、再執行解構與異常處理;先發完全部中斷,各執行緒可並行處理自己的中斷,整體退出更快。
另需在包裝 lambda 中 catch (thread_interrupted const&):未捕獲的中斷異常傳進 std::thread 解構會呼叫 std::terminate() 終止整個程式。
Question 9 - 中斷條件變數等待的競爭 [analysis]
天真版
interruptible_wait(cv, lk)(代碼 9.10)有哪兩個缺陷?比較wait_for(1ms)輪詢(代碼 9.11)與condition_variable_any+ custom_lock(代碼 9.12)兩種解法的取捨。
兩缺陷:(1) 異常安全 —— cv.wait() 可能拋出異常而未清除旗標與條件變數的關聯(修法:RAII clear_cv_on_destruct);(2) 競爭條件 —— 最後一次檢查與 wait() 進入等待之間若旗標被設定並 notify_all(),通知遺失,執行緒再也叫不醒。
wait_for(1ms):簡單、適用 std::condition_variable,但通知遺失最多晚 1ms 才發現,且虛假喚醒與述詞檢查次數大增。
custom_lock 版:全程持有 set_clear_mutex,而 set() 必須取得同一把鎖才能通知 → 通知不可能落在空窗,無需輪詢;代價是只能用於 std::condition_variable_any,且鎖的協定較複雜lock 同時鎖兩把避免死結。
Question 10 - 任務佇列設計的演進 [analysis]
比較執行緒池三代任務佇列設計:單一全域佇列、
thread_local本地佇列、本地佇列 + 工作竊取。各解決什麼問題?又引入什麼新問題?
| 設計 | 解決 | 新問題 |
|---|---|---|
| 單一全域佇列 | 實作最簡單 | 執行緒越多爭用越重;即使無鎖佇列仍有快取乒乓 |
| thread_local 本地佇列 | 本地存取無鎖無爭用 | 任務分配不均:某執行緒佇列爆滿,其他執行緒閒置 |
| 本地 + 工作竊取 | 閒置執行緒可從他人後端偷任務,兼顧低爭用與負載平衡 | 本地佇列需可被他人存取(需同步);竊取自 my_index+1 輪詢以分散熱點 |
取任務順序本地 → 全域 → 竊取反映成本由低到高;書中仍留有改進空間(如動態調整池大小)。
| 關鍵字 | 答案 |
|---|---|
| 成員宣告順序 done/queue → threads → joiner | 反序銷毀:joiner 先匯入,worker 存取的成員最後銷毀 |
| 任務粒度太細 | 提交開銷不划算,可能比單執行緒還慢;工作量依塊數而非執行緒數劃分 |
packaged_task 入列 |
先 get_future(),再 std::move 推入,回傳 future |
| 等待相依任務 | wait_for(0s) 輪詢 + run_pending_task() 防死結 |
| 本地佇列空指標 | 代表非池內執行緒 → submit() 落全域佇列 |
| 擁有者 LIFO / 竊取者 FIFO | 快取親和 / 減少兩端競爭 |
| 中斷 = 異常 | thread_interrupted;沒人接 → std::terminate() |
| 中斷阻塞等待 | cv:wait_for(1ms) 或 custom_lock;future:迴圈 wait_for(1ms)(受時鐘刻度限制) |
| C++20 對應 | std::jthread + std::stop_token(P0660) |