執行策略 (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::findstd::transformstd::reduce)的重載。簽名與單執行緒版本相同,只在參數列表最前面新增一個執行策略參數:

std::vector<int> my_data;
std::sortpar, my_data.begin(), my_data.end();

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::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 與「不帶策略」重載的關鍵差異。

影響三:執行的位置與時間

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 seqpar 可以;par_unseq 絕對不行(未定義行為)
操作可交錯、可跨執行緒遷移的策略 par_unseq
par 下用普通 int 計數器 data race → UB,且是呼叫的靜態屬性(與庫是否真的開執行緒無關)
指定 par 就保證平行執行? ,策略是許可;實作仍可串行執行
帶策略版本操作次數變多,倍數取決於 庫實作與平台,不是資料