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++98(1998):標準不承認執行緒的存在,語義以循序抽象的形式編寫,也沒有記憶體模型——缺少編譯器擴展就無法編寫多執行緒應用。
- 編譯器擴展/平台 API:供應商透過平台相關擴展支援多執行緒,主流 API 為 POSIX 標準的 C 介面與 Microsoft Windows API。支援受限於平台,且需平台執行期庫(如異常處理機制)在多執行緒下正常運作。在正式記憶體模型出現前,開發者靠編譯器與處理器的「實際表現不錯」寫了大量 C++ 多執行緒程式。
- C++ 類別庫:不滿足於平台 C API,出現了 MFC 應用框架與 Boost、ACE 等通用庫。細節差異大,但啟動執行緒的方式大同小異,共通設計:帶鎖的 RAII(Resource Acquisition Is Initialization)。
- 遺留問題:缺乏統一標準與記憶體模型,跨平台多執行緒應用問題最明顯。
C++11:標準支援並發
C++11 帶來兩件大事:
- 全新的記憶體模型(多執行緒語義的根基,參見第5章)
- 標準庫擴展:
- 管理執行緒(參見第2章)
- 保護共享資料(參見第3章)
- 執行緒間同步操作(參見第4章)
- 原子操作(參見第5章)
- 標準執行緒庫大量吸收既有 C++ 庫的經驗——尤其 Boost 執行緒庫:許多類別同名同結構;Boost 也隨標準演進反向調整,Boost 使用者會對 C++11 執行緒庫非常熟悉。
- C++11 同時改善了語言本身(移動語義、lambda 等,參見附錄A),其中不少特性深刻影響執行緒庫的使用方式。
- 標準直接支援原子操作:開發者以指定語義的方式編寫代碼,無需了解平台相關的組合語言指令;編譯器可跨平台處理並讓最佳化器理解操作語義,程式獲得更好的最佳化。
C++14 與 C++17 的更多支援
- C++14:新增一個用於保護共享資料的新互斥鎖類型(參見第3章)。
- C++17:新增一整套平行演算法(參見第10章)。
- 並發技術規範 (Concurrency TS):描述對標準函式與類別的擴展,特別是執行緒同步方面(參見第4章)。
C++ 執行緒庫的效率:抽象懲罰 (abstraction penalty)
抽象懲罰:實現相同功能時,使用高階工具與底層工具之間的開銷差。這是高效能計算開發者對 C++ 執行緒庫的主要疑慮。
- 標準委員會的設計目標:高階 API 與底層 API 的效能收益相當——標準庫在主流平台上都能有高效實現(極低的抽象懲罰)。
- 為了「終極效能」,提供了原子操作庫這類底層工具:直接控制單一位元、位元組、執行緒間同步與變更的可見性;使用標準原子類型的代碼兼具可移植性與可維護性。
- 高階工具(更易用)確有額外代碼要執行,但整體開銷與手工編寫的等效功能差不多,且編譯器會內聯大部分代碼。
- 極少情況下,高階工具未使用到的附加功能會影響其他代碼的效能——若很看重效能且高階工具開銷過高,才改用底層工具實作。
Warning
用過高的複雜度與出錯率去換小幅效能收益,絕大多數情況下不划算。即便瓶頸出現在標準庫工具上,元凶也常是低劣的程式設計——例如過多執行緒競爭同一個互斥鎖;與其最佳化互斥操作,不如重新設計以減少競爭(參見第8章)。
平台相關的工具:native_handle()
某些平台提供標準庫之外的額外工具。為了在使用標準 C++ 執行緒庫的同時也能取用它們,執行緒庫的類別提供 native_handle() 成員函式,允許以平台相關 API 直接操作底層實作。
- 透過
native_handle()所做的任何操作完全依賴平台,超出標準 C++ 庫(與本書)的範圍。
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 C、Windows 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() |
| 「互斥鎖競爭太兇,先做什麼」 | 重新設計減少競爭,而非怪罪標準庫 |
Related Notes
- 01-Concurrency-Fundamentals/01-What-Is-Concurrency
- 01-Concurrency-Fundamentals/02-Why-Use-Concurrency
- 01-Concurrency-Fundamentals/Practice-Concurrency-Fundamentals
- 02-Thread-Management/01-Launching-and-Joining-Threadsthread、
join()/detach()細節 - 05-Memory-Model-and-Atomics/01-Memory-Model-Basics(C++11 記憶體模型)
- 10-Parallel-Algorithms/01-Execution-Policies(C++17 平行演算法)
- [12-Appendix/01-Cpp11-Language-Features|12-Appendix/01-Cpp11-Language-Features]