執行緒管理練習題 (Practice - Thread Management)
Related Concepts
- 02-Thread-Management/01-Launching-and-Joining-Threads
- 02-Thread-Management/02-Passing-Arguments-and-Transferring-Ownership
- 02-Thread-Management/03-Thread-Count-and-Identification
| 關鍵字 | 答案 |
|---|---|
| 解構前未 join/detach | std::terminate() |
| 傳臨時函式物件被當函式宣告 | most vexing parse → {}、雙括號、lambda |
| 執行緒內要改呼叫端變數 | std::ref(data) |
char* 區域緩衝區傳入 + detach |
先轉 std::string 再傳 |
| 只可移動型別傳入 | std::move()(臨時物件自動隱式移動) |
| 賦值給已關聯執行緒的 thread | std::terminate() |
| hardware_concurrency() == 0 | 無法取得,自選預設值(如 2) |
| 判斷 master thread | 比對 std::this_thread::get_id() |
Question 1 - 解構前的抉擇 [recall]
一個
std::thread物件在關聯的執行緒仍在執行時被解構,且未曾呼叫 join() 或 detach(),會發生什麼事?
std::thread 的解構函式會呼叫 std::terminate(),整個程式立即終止。因此即使有例外存在,也必須確保執行緒在物件解構前被 join(匯入) 或 detach(分離)。
Question 2 - Most Vexing Parse [recall]
std::thread my_thread(background_task());這行程式碼有什麼問題?列出至少兩種修正方式。
它被編譯器解析為函式宣告(most vexing parse):宣告一個回傳 std::thread、參數為函式指標的函式 my_thread——執行緒根本沒啟動。修正方式:
- 多一組括號:
std::thread my_thread((background_task())); - 統一初始化語法:
std::thread my_thread{background_task()}; - 使用命名變數或 lambda。
Question 3 - join 的次數與 joinable [recall]
對同一個
std::thread物件可以呼叫幾次 join()?join 之後 joinable() 回傳什麼?
join() 只能呼叫一次。join 會清理執行緒相關記憶體,此後 std::thread 物件與已完成的執行緒不再有任何關聯,joinable() 回傳 false,不能再次匯入(detach 後同樣回傳 false)。
Question 4 - 參數傳遞的本質 [recall]
std::thread建構函式傳遞參數時,若目標函式的參數宣告為widget_data&(非 const 參考),直接傳入變數會發生什麼事?如何修正?
參數一律被拷貝到新執行緒的儲存空間,內部再以右值傳遞;右值無法綁定非 const 左值參考,因此編譯錯誤。修正:以 std::ref(data) 包裝,函式才會收到 data 的參考thread 與 std::bind 採相同機制。
Question 5 - hardware_concurrency [recall]
std::thread::hardware_concurrency()回傳什麼?回傳 0 代表什麼意思?
回傳系統可真正並行的執行緒數量(多核心系統上通常為 CPU 核心數),但僅是一個提示。回傳 0 代表無法取得該資訊,此時呼叫端應自選合理預設值(書中範例選 2)。
Question 6 - 執行緒識別 [recall]
取得
std::thread::id有哪兩種方式?當std::thread物件未關聯任何執行緒時,get_id() 回傳什麼?
- 透過物件成員函式
t.get_id();2. 在當前執行緒中呼叫std::this_thread::get_id()(定義於<thread>)。未關聯執行緒時,get_id()回傳預設建構的std::thread::id,表示「無執行緒」。std::thread::id可拷貝、比較、排序,也有std::hash特化,可當有序/無序容器的鍵。
Question 7 - 懸空指標除錯 [application]
同事寫了
char buffer[1024]; sprintf(buffer, "%i", some_param); std::thread t(f, 3, buffer); t.detach();,其中 f 的第二參數是std::string const&。程式偶爾輸出亂碼,為什麼?怎麼改?
拷貝進執行緒的是 char* 指標,buffer → std::string 的隱式轉換發生在新執行緒的上下文,可能在函式返回、buffer 銷毀之後才執行 → 懸空指標、未定義行為。修正:傳入前先顯式轉換——std::thread t(f, 3, std::string(buffer));。
Question 8 - 只可移動型別與批次管理 [application]
你要把一個
std::unique_ptr<big_object> p(命名變數)交給新執行緒處理,並且一次啟動 20 條執行緒、全部等待結束。寫出關鍵程式碼。
命名變數必須顯式移動:std::thread t(process_big_object, std::move(p));(臨時物件才會自動隱式移動)。批次管理用容器:
std::vector<std::thread> threads;
for (unsigned i = 0; i < 20; ++i) threads.emplace_back(do_work, i);
for (auto& entry : threads) entry.join();
Question 9 - thread_guard vs scoped_thread [analysis]
比較
thread_guard(持參考)與scoped_thread(持所有權)兩種 RAII 設計:檢查時機、解構行為、誤用風險各有何差異?哪個標準型別是它們的後繼者?
- thread_guard:持
std::thread&,解構函式中檢查joinable()才 join;原 thread 物件仍存在,可能被外部誤用(如再 detach)。 - scoped_thread:以
std::move接管所有權,檢查提前到建構函式——不可匯入即拋std::logic_error;解構時無條件 join,原物件已被移走,杜絕誤用。錯誤更早暴露,語意更安全。 - 兩者皆刪除拷貝操作。標準化後繼者為 C++20
std::jthread(書中的 joining_thread;C++17 提案未達共識)。
Question 10 - parallel_accumulate 的數量選擇 [analysis]
代碼 2.9 用
num_threads = min(hardware_threads != 0 ? hardware_threads : 2, max_threads)決定執行緒數,且只啟動num_threads-1條。分析:(a) 為何取 min?(b) 為何少開一條?(c) 為何對 float 的結果可能與std::accumulate不同?
- (a)
max_threads = (length+min_per_thread-1)/min_per_thread保證每條執行緒有足夠工作量;與硬體執行緒數取 min 是因為超過硬體支援只會增加上下文切換、降低效能(oversubscription)。 - (b) 啟動前主執行緒已存在且會處理最終塊,所以只需再開
num_threads-1條。 - (c) 浮點加法不滿足結合律(截斷誤差),分塊改變了累加順序,結果可能與序列版不同。另外此演算法要求前向迭代器、T 有預設建構函式。
| 關鍵字 | 答案 |
|---|---|
| 未 join/detach 即解構 | std::terminate() |
| join 次數 | 一次;之後 joinable() == false |
| 臨時函式物件當參數 | most vexing parse → {}/雙括號/lambda |
| 非 const 參考參數 | std::ref;成員函式 → f, &obj, ... |
| 區域 char buffer | 先 std::string(buffer) 再傳 |
| unique_ptr / thread 本身 | 可移動不可拷貝;命名變數要 std::move |
| 賦值給已關聯執行緒 | std::terminate();不能用賦值丟棄執行緒 |
| 執行緒數公式 | min(hardware(0→預設2), ceil(length/min_per_thread));主執行緒算一條 |
| 浮點平行加總 | 不滿足結合律 → 結果可能不同 |
| master thread 判斷 | this_thread::get_id() == 預存 id |