執行策略 (Execution Policies)
Overview Table
| 概念 | 重點 |
|---|---|
| C++17 平行演算法 | 既有標準演算法的重載版本,新增第一個參數 = 執行策略 |
| 執行策略的三大影響 | 複雜度放寬、異常行為改變、執行位置/方式/時間由策略決定 |
| 異常行為 | 異常逃逸出演算法 → 一律 std::terminate(唯一例外:庫內部資源不足拋 std::bad_alloc) |
std::execution::seq |
單一執行緒、不交錯,但元素順序未指定;可自由使用同步機制 |
std::execution::par |
多執行緒;不可有 data race,但允許 mutex / atomic 同步 |
std::execution::par_unseq |
最大平行化;操作可交錯、跨執行緒遷移;禁止任何同步 |
10.1 C++17 平行化標準庫演算法總覽
C++17 為標準庫加入平行演算法:並非新演算法,而是對既有演算法(如 std::find、std::transform、std::reduce)的重載。簽名與單執行緒版本相同,只在參數列表最前面新增一個執行策略參數:
std::vector<int> my_data;
std::sortpar, my_data.begin(), my_data.end();
std::execution::par表示允許以多執行緒執行;實作仍可以串行方式執行(策略是許可,不是保證)。- 指定執行策略後,演算法的複雜度要求比串行版寬鬆:平行演算法為了利用平行性通常會做更多總工作量——若能分攤到 100 個處理器,即使總工作量是原來的 2 倍,仍可獲得約 50 倍加速。
10.2 三種標準執行策略(比較表)
三個策略類別定義在 <execution> 標頭檔,並各自對應一個可直接傳入演算法的策略物件:
sequenced_policy |
parallel_policy |
parallel_unsequenced_policy |
|
|---|---|---|---|
| 策略物件 | std::execution::seq |
std::execution::par |
std::execution::par_unseq |
| 執行緒 | 全部操作在呼叫演算法的執行緒上 | 呼叫執行緒 + 庫建立的執行緒 | 任意執行緒,無順序保證 |
| 操作交錯 | 不交錯,有確定先後(但順序未指定) | 同一執行緒內不交錯;給定操作固定在同一執行緒上完成 | 可在單一執行緒內交錯,可在執行緒間遷移(在 A 啟動、B 執行、C 完成) |
| 元素存取順序 | 未指定,且與無策略重載不保證相同 | 未指定,不同呼叫可能不同 | 完全未指定 |
| 可否使用同步機制(mutex/atomic) | 可以,可自由使用;可依賴「所有操作在同一執行緒」 | 可以(元素間允許同步存取共享狀態) | 不可以:不能使用任何同步,也不能呼叫任何內部需要同步的函式 |
| 對迭代器/值/可呼叫物件的要求 | 幾乎無額外要求(仍不可依賴操作順序) | 不得產生 data race;不得依賴其他操作在(或不在)同一執行緒上執行 | 只能操作元素本身(或由其可及的資料),不得修改執行緒間或元素間的共享狀態 |
| 典型加速潛力 | 無(仍是單執行緒) | 多執行緒平行 | 平行 + 向量化(SIMD)空間最大 |
seq : T0: [op1]──[op2]──[op3]──[op4] 單執行緒、不交錯、順序未指定
par : T0: [op1]──[op3] T1: [op2]──[op4] 多執行緒;單一操作不跨執行緒
par_unseq : T0: [op1a op2a op1b ...] T1: [op2b ...]
操作可交錯、可在執行緒間遷移
使用規則
- 策略物件只能複製,不能自行建構(有特殊初始化要求);實作可以提供額外的非標準策略,但開發者不能自訂執行策略。
- 「是否有未定義行為」是呼叫的靜態屬性:用
par寫出有 data race 的程式碼,即使庫實際上沒開多執行緒,依然是未定義行為。
10.2.1 執行策略對演算法行為的三大影響
影響一:演算法複雜度
- 除了平行管理的排程開銷外,核心操作(交換、比較、使用者提供的函式物件)可能被執行更多次,以換取總執行時間縮短。
- 帶策略重載的操作次數可能是無策略版本的數倍;倍數取決於庫實作與平台,而非傳入的資料。
影響二:異常時的行為
- 執行期間若有異常未被捕獲而逃逸,所有標準執行策略都會呼叫
std::terminate,直接終止程式。 - 使用標準策略時演算法唯一會拋出的異常是
std::bad_alloc——當標準庫無法為內部操作取得足夠資源時。 - 對比:無執行策略的版本會正常傳播異常。
std::for_each(v.begin(), v.end(),
[](auto x){ throw my_exception(); }); // 異常向外傳播
std::for_eachseq, v.begin(), v.end(,
[](auto x){ throw my_exception(); }); // std::terminate!
seq 也會 terminate
std::execution::seq 雖是單執行緒執行,但它仍是執行策略:複雜度與異常語義和其他標準策略相同。「異常逃逸 → terminate」對 seq/par/par_unseq 一體適用,這正是 seq 與「不帶策略」重載的關鍵差異。
影響三:執行的位置與時間
- 這是各標準策略間根本的差異:策略指定用哪些代理 (agent) 執行演算法——普通執行緒、向量流、GPU 執行緒等。
- 策略也約束演算法步驟的安排:是否按特定順序、步驟間能否交錯、能否彼此平行。
10.2.2 順序策略的陷阱:順序仍未指定
seq 強制所有操作在同一執行緒上、不交錯地執行,但造訪元素的具體順序未指定,且不同呼叫間可能不同:
std::vector<int> v(1000);
int count = 0;
std::for_eachseq, v.begin(), v.end(,
[&](int& x){ x = ++count; }); // 不保證 v 是 1..1000 依序排列!
不要依賴
seq 的存取順序
上例在 seq 下沒有 data race(單執行緒),但不能依賴數字按位置順序存入;無策略的 std::for_each 才保證依序。若同樣程式碼改用 par,count 就是 data race → 未定義行為(修正法:改用 std::atomic<int> 或 mutex,但會將呼叫序列化,失去平行效益)。
Exam/Test Patterns
| 情境/關鍵字 | 答案 |
|---|---|
| 平行演算法與串行版簽名差在哪 | 新增第一個參數:執行策略;其餘參數相同 |
| 三個標準策略/物件、定義在哪 | sequenced_policy/parallel_policy/parallel_unsequenced_policy;物件 seq/par/par_unseq;皆在 <execution> |
| 可以自己建構/自訂執行策略嗎 | 不行:物件只能複製;實作可加策略,開發者不能自訂 |
| 帶策略的演算法中異常逃逸 | 一律 std::terminate(含 seq);無策略版本才會傳播異常 |
| 帶標準策略時唯一可能拋出的異常 | std::bad_alloc(庫內部資源不足) |
| 演算法內想用 mutex / atomic | seq、par 可以;par_unseq 絕對不行(未定義行為) |
| 操作可交錯、可跨執行緒遷移的策略 | par_unseq |
par 下用普通 int 計數器 |
data race → UB,且是呼叫的靜態屬性(與庫是否真的開執行緒無關) |
指定 par 就保證平行執行? |
否,策略是許可;實作仍可串行執行 |
| 帶策略版本操作次數變多,倍數取決於 | 庫實作與平台,不是資料 |