執行緒數量與識別 (Thread Count and Identification)
Overview Table
| 概念 | 重點 |
|---|---|
hardware_concurrency() |
回傳可真正並行的執行緒數(如 CPU 核心數);僅為提示,無法取得時回傳 0 |
| 執行緒數量公式 | num_threads = min(硬體執行緒數(0 則取預設值), max_threads);避免過多執行緒造成上下文切換 |
| parallel_accumulate | 分塊 → 啟動 num_threads-1 條(主執行緒算最後一塊)→ 全部 join → 彙總 |
| 使用限制 | 浮點加法不滿足結合律 → 結果可能異於 std::accumulate;需前向迭代器、T 有預設建構函式 |
std::thread::id |
get_id() / std::this_thread::get_id();可拷貝、比較、排序、雜湊,適合當容器鍵 |
確定執行緒數量:hardware_concurrency()
std::thread::hardware_concurrency() 回傳系統可並行的執行緒數量——多核心系統上通常是 CPU 核心數。
- 回傳值僅是一個提示 (hint)
- 無法取得資訊時回傳 0,呼叫端須自選合理預設值(書中範例選 2)
- 執行緒數量超過硬體支援(oversubscription)會造成上下文切換頻繁、效能下降
不要直接拿回傳值當執行緒數:一要處理回傳 0 的情況,二要與「工作量能支撐的最大執行緒數」取小,免得資料太少卻開一堆執行緒。
平行版 accumulate(代碼 2.9)
把整體工作拆成小塊分給各執行緒,並設定最小任務量避免執行緒過多:
length = distance(first, last) ── 空範圍?→ 直接回傳 init
max_threads = (length + min_per_thread - 1) / min_per_thread // 天花板除法
hardware_threads = std::thread::hardware_concurrency()
num_threads = min(hardware_threads != 0 ? hardware_threads : 2, max_threads)
block_size = length / num_threads
std::vector<T> results(num_threads);
std::vector<std::thread> threads(num_threads - 1); // 主執行緒也算一條!
Iterator block_start = first;
for (unsigned long i = 0; i < (num_threads - 1); ++i) {
Iterator block_end = block_start;
std::advance(block_end, block_size);
threads[i] = std::thread(accumulate_block<Iterator,T>(),
block_start, block_end, std::ref(results[i]));
block_start = block_end;
}
accumulate_block<Iterator,T>()(block_start, last, results[num_threads-1]); // 主執行緒處理最終塊
for (auto& entry : threads) entry.join();
return std::accumulate(results.begin(), results.end(), init);
流程總覽:
輸入範圍 ──分塊──▶ 塊1 塊2 ... 塊N-1 最終塊
│ │ │ │
執行緒1 執行緒2 執行緒N-1 主執行緒(⑨)
│ │ │ │
└────┴── join 全部 ────────┘
│
accumulate(results) → 總結果
- 啟動
num_threads - 1條:啟動前已有主執行緒,主執行緒負責最終塊(所以最終塊元素數多寡無所謂) - 中間結果存
std::vector<T> results,以std::ref(results[i])傳參考進執行緒——執行緒無法直接回傳值 - 操作數量為 0 時可能拋出例外(如
std::thread無法啟動執行緒時)
注意事項(使用限制)
- T 的加法若不滿足結合律(如
float/double的截斷誤差),分塊會使結果與std::accumulate不同 - 迭代器要求更嚴格:必須是前向迭代器accumulate 只需輸入迭代器
results容器要求 T 有預設建構函式- 取得執行緒結果的更佳方案是 future(第 4 章);C++17 的
std::reduce即等價此處的 parallel_accumulate(第 10 章)
執行緒識別:std::thread::id
執行緒識別碼型別為 std::thread::id,取得方式有兩種:
| 方式 | 寫法 | 說明 |
|---|---|---|
| 從物件取得 | t.get_id() |
若物件未關聯執行緒,回傳預設建構值,代表「無執行緒」 |
| 從當前執行緒取得 | std::this_thread::get_id() |
定義於 <thread>,取得自己的 id |
性質:
- 可自由拷貝與比較:相等 ⇒ 同一執行緒或都「無執行緒」;不等 ⇒ 不同執行緒或一有一無
- 提供全序比較(
a<b、b<c⇒a<c)→ 可當有序容器的鍵、可排序 - 標準庫提供
std::hash<std::thread::id>→ 可當無序容器的鍵 - 可直接輸出:
std::cout << std::this_thread::get_id();——輸出格式依實作而定,標準只保證相同 id 輸出相同
典型用途
- 主執行緒判斷:啟動其他執行緒前記下自己的 id,演算法中比對以決定是否執行主執行緒專屬工作
std::thread::id master_thread;
void some_core_part_of_algorithm() {
if get_id() == master_thread
do_master_thread_work();
do_common_work();
}
- 存進資料結構比對當前執行緒,決定操作是「允許」還是「需要」(permitted/required)
- 當容器鍵值:替執行緒關聯專屬資訊(執行緒本地儲存不適用時的替代方案)、或於執行緒間互傳資訊
Exam/Test Patterns
| 情境/關鍵字 | 答案 |
|---|---|
hardware_concurrency() 回傳 0 |
代表無法取得資訊;自選預設值(書中用 2) |
| 「該開幾條執行緒?」 | min(硬體執行緒數, (length+min_per_thread-1)/min_per_thread)——工作量與硬體取小 |
parallel_accumulate 為何只啟動 num_threads-1 條 |
主執行緒也是一條,負責處理最終塊 |
| float 平行加總結果與序列版不同 | 浮點加法不滿足結合律,分塊改變運算順序 |
| 「執行緒怎麼把結果傳回來?」 | 不能直接回傳:傳 std::ref 參考進去,或用 future(第 4 章) |
| 判斷目前是否為 master thread | 比對 std::this_thread::get_id() 與預存的 std::thread::id |
std::thread::id 能否當 map/unordered_map 的鍵 |
可以:支援全序比較與 std::hash<std::thread::id> |
未關聯執行緒的 t.get_id() |
回傳預設建構的 id,表示「無執行緒」 |