C++ 並發支援與第一個多執行緒程式 (C++ Concurrency Support and First Program)

Overview Table

概念 重點
C++98 不承認執行緒存在、無記憶體模型 → 沒有編譯器擴展就寫不了多執行緒
前 C++11 時代 平台 API(POSIX C、Windows API)+ 類別庫(MFC、Boost、ACE,RAII 帶鎖)
C++11 全新記憶體模型 + 標準執行緒庫(thread/共享資料保護/同步/atomic)
C++14 / C++17 新互斥鎖類型 / 一整套平行演算法;另有並發技術規範 (Concurrency TS)
抽象懲罰 (abstraction penalty) 高階工具 vs 底層工具的開銷差;C++ 設計目標是極低
native_handle() 以平台相關 API 直接操作底層實作的逃生口
Hello Concurrent World #include <thread> → 起始函式 → std::thread t(hello)t.join()

C++ 多執行緒發展史

C++98 ────────► 平台 API 時代 ─────────► C++11 ──────► C++14 ──────► C++17
不承認執行緒     POSIX C / Windows API     記憶體模型     新互斥鎖類型   平行演算法
無記憶體模型     MFC / Boost / ACE 類別庫   標準執行緒庫   (第3章)       (第10章)
                (RAII 帶鎖)                              + 並發技術規範 (Concurrency TS)

C++11:標準支援並發

C++11 帶來兩件大事:

  1. 全新的記憶體模型(多執行緒語義的根基,參見第5章)
  2. 標準庫擴展:
    • 管理執行緒(參見第2章)
    • 保護共享資料(參見第3章)
    • 執行緒間同步操作(參見第4章)
    • 原子操作(參見第5章)

C++14 與 C++17 的更多支援

C++ 執行緒庫的效率:抽象懲罰 (abstraction penalty)

抽象懲罰:實現相同功能時,使用高階工具底層工具之間的開銷差。這是高效能計算開發者對 C++ 執行緒庫的主要疑慮。

Warning

過高的複雜度與出錯率去換小幅效能收益,絕大多數情況下不划算。即便瓶頸出現在標準庫工具上,元凶也常是低劣的程式設計——例如過多執行緒競爭同一個互斥鎖;與其最佳化互斥操作,不如重新設計以減少競爭(參見第8章)。

平台相關的工具:native_handle()

某些平台提供標準庫之外的額外工具。為了在使用標準 C++ 執行緒庫的同時也能取用它們,執行緒庫的類別提供 native_handle() 成員函式,允許以平台相關 API 直接操作底層實作

Tip

使用平台相關工具之前,先了解標準庫能做什麼——標準庫做得到,就別綁死平台。

Hello Concurrent World:逐行解說

多執行緒的 C++ 程式和一般 C++ 程式差不多,唯一區別:某些函式可以並發執行,因此要確保共享資料在並發存取時安全,並用特定的函式與物件來管理執行緒

#include <iostream>
#include <thread>              // (1) 多執行緒支援的標頭

void hello()                   // (2) 新執行緒的起始函式
{
    std::cout << "Hello Concurrent World\n";
}

int main()
{
    std::thread t(hello);      // (3) 啟動新執行緒
    t.join();                  // (4) 等待新執行緒結束
}
# 代碼 說明
1 #include <thread> 管理執行緒的函式與類別宣告於 <thread>;保護共享資料的設施宣告在其他標頭
2 void hello() 每個執行緒都必須有一個起始函式,新執行緒從這裡開始執行;初始執行緒的起始函式是 main()
3 std::thread t(hello); std::thread 建構函式中指定起始函式即啟動新執行緒——執行緒數量一分為二:初始執行緒始於 main(),新執行緒始於 hello()
4 t.join(); 新執行緒啟動後初始執行緒繼續執行;若不等待,main() 可能先結束——甚至發生在新執行緒執行之前。join() 讓建立方等待 std::thread 所啟動的執行緒結束(詳見第2章)
Warning

若省略 join(),main() 可能在新執行緒執行前就結束。且thread 物件解構時若既未 join() 也未 detach(),會呼叫 std::terminate( 終止程式。

Tip

這個例子只為印一行字就開執行緒——如 1.2.3 節所言,如此簡單的任務不值得使用多執行緒(啟動開銷 > 任務本身),尤其初始執行緒期間無事可做。C++ 使用多執行緒不複雜,複雜的是設計代碼以達到預期行為


Exam/Test Patterns

情境/關鍵字 答案
「C++98 能寫可移植的多執行緒?」 不能——不承認執行緒、無記憶體模型,需編譯器/平台擴展
「C++11 前的多執行緒選項」 平台 API(POSIX CWindows API)或類別庫(MFC、Boost、ACE)
「前 C++11 類別庫的共通設計」 帶鎖的 RAII
「C++11 為並發帶來什麼」 新記憶體模型 + 執行緒管理/共享資料保護/同步/原子操作標準庫
「C++11 執行緒庫以哪個庫為藍本」 Boost 執行緒庫(類別同名同結構)
「C++14 / C++17 對並發的新增」 互斥鎖類型 / 一整套平行演算法
「abstraction penalty」 高階工具 vs 底層工具的開銷差;C++ 標準庫目標為極低
「想用平台特有的執行緒功能」 native_handle() 成員函式(完全平台相依)
「執行緒管理設施宣告在哪」 <thread> 標頭
「新執行緒從哪裡開始執行」 std::thread 建構函式指定的起始函式
「不呼叫 join() 會怎樣」 main() 可能先結束;未 join/detach 的 std::thread 解構會 std::terminate()
「互斥鎖競爭太兇,先做什麼」 重新設計減少競爭,而非怪罪標準庫