異常安全與可擴展性 (Exception Safety and Scalability)

Overview Table

主題 關鍵點
平行演算法異常安全 新執行緒上的異常無法自動傳回呼叫者;裸 std::thread 版會 std::terminate
逐步改造 packaged_task/future 轉移異常 → join_threads RAII → std::async 版天然安全
Amdahl 定律 P = 1 / (fs + (1 - fs) / N);串行部分 fs 決定加速上限 1/fs
隱藏延遲 執行緒等待(I/O、鎖)時讓 CPU 跑別的工作:加執行緒、非同步 I/O、自己動手做
提升響應能力 GUI 事件迴圈獨立成專用執行緒,長任務丟給任務執行緒 + atomic<bool> 取消旗標

平行演算法的異常安全

串行演算法中異常可直接傳給呼叫者處理;平行演算法的操作跑在別的執行緒上,異常傳不過去。若函數帶著新執行緒異常退出,std::thread 解構時仍 joinable → std::terminate 終止程式

原始版的漏洞(代碼 8.2,裸 std::thread + 結果陣列)

拋出點 後果
主執行緒:產生第一條執行緒後再拋(如建構後續 std::thread 失敗) 已建的 std::thread 物件解構 → std::terminate
工作執行緒:accumulate_block 內拋出 無 catch → std::terminate
全部 join 之後拋出 無害(執行緒已匯合)

改造一:std::packaged_task + std::future 轉移異常(代碼 8.3)

std::packaged_task<T(Iterator,Iterator)> task(accumulate_block<Iterator,T>());
futures[i] = task.get_future();
threads[i] = std::threadmove(task), block_start, block_end;
// ...join 後:
result += futures[i].get();   // 工作執行緒的異常在這裡重新拋出

改造二:join_threads RAII(代碼 8.4)

class join_threads {                    // thread_guard 的向量版
  std::vector<std::thread>& threads;
public:
  explicit join_threadsthread>& t) : threads(t {}
  ~join_threads() {
    for (auto& t : threads)
      if (t.joinable()) t.join();
  }
};

改造三:std::async 版——天然異常安全(代碼 8.5)

template <typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init) {
  unsigned long const length = std::distance(first, last);
  unsigned long const max_chunk_size = 25;
  if (length <= max_chunk_size)
    return std::accumulate(first, last, init);
  Iterator mid_point = first;
  std::advance(mid_point, length / 2);
  auto first_half = std::async(parallel_accumulate<Iterator, T>,
                               first, mid_point, init);      // 非同步處理前半
  T second_half = parallel_accumulate(mid_point, last, T()); // 遞迴處理後半
  return first_half.get() + second_half;                     // 異常在 get() 傳播
}
異常安全是正確性問題

可擴展性差只是慢,不是錯;異常不安全則會破壞不變量、產生競爭條件,甚至讓程式直接 std::terminate——優先級最高。

可擴展性與 Amdahl 定律

可擴展性 (scalability):核心數增加時,程式能否等比提升效能(縮短單一動作時間,或在同時間內做更多工作)。單執行緒程式完全不可擴展;理想是 100 核 = 100 倍效能。

把程式簡化為「串行部分(只能單執行緒做)+ 並行部分(所有處理器可分工)」,串行部分占比為 fs,處理器數為 N,則效能增益:

              1
P = ─────────────────────      (Amdahl 定律)
      fs + (1 - fs) / N
簡化模型

Amdahl 定律假設 fs 固定、忽略同步/通訊開銷與快取效應;實務上執行緒還會互等、等 I/O,實際增益通常更低。它是「上限估計」而非預測值。

用多執行緒隱藏延遲

執行緒常因等待(I/O、mutex、條件變數)而阻塞——阻塞就是把 CPU 時間讓出來。若系統還有工作可做,就能把等待「隱藏」起來:

用併發提升響應能力(GUI 事件執行緒分離)

GUI 框架多為事件驅動 (event driven):單執行緒下長任務必須手動切碎、定期回到事件迴圈,否則介面凍結。用關注點分離解決——專用 GUI 執行緒處理事件,長任務交給任務執行緒:

std::atomic<bool> task_cancelled(false);
// GUI 執行緒:  while(true){ event=get_event(); if(quit)break; process(event); }
// 任務執行緒:  while(!task_complete() && !task_cancelled) do_next_operation();
//              被取消→perform_cleanup();完成→post_gui_event(task_complete);
// process():  start_task→ 開 task_thread;stop_task→ task_cancelled=true 後 join;
//              task_complete→ join 後 display_results();

Exam/Test Patterns

情境/關鍵字 答案
「工作執行緒拋異常,程式直接掛掉」 無人捕捉 → std::terminate;用 packaged_task/promise 把異常存進 future,主執行緒 get() 重拋
「產生執行緒後主執行緒拋異常」 joinable 的 std::thread 解構 → terminate;用 join_threads RAII 保證匯合
「最簡潔的異常安全平行累加」 std::async 遞迴對半劃分:future 解構等待執行緒、get() 傳播異常,天然安全
「fs = 串行占比,N 無限大,加速上限?」 P = 1/(fs+(1-fs)/N) → 上限 1/fs(fs=1/3 → 最多 3 倍)
「執行緒被 I/O 卡住,CPU 閒置」 隱藏延遲:加執行緒 / 非同步 I/O / 等待者自己執行任務
「GUI 跑長任務會凍結」 分離關注點:專用事件執行緒 + 任務執行緒;atomic<bool> 取消、事件回傳完成通知
「可擴展性 vs 異常安全哪個是正確性問題」 異常安全;不可擴展只是效能差,程式仍正確