多執行緒執行與資料競爭 (C++11 起)
一個 執行執行緒 是程式內的一個控制流,始於特定頂層函式的呼叫(透過 std::thread、std::async、std::jthread(C++20 起) 或其他方式),並遞迴地包含隨後由該執行緒執行的每個函式呼叫。
- 當一個執行緒建立另一個執行緒時,新執行緒的頂層函式的初始呼叫由新執行緒執行,而不是由建立執行緒執行。
任何執行緒都可以潛在地訪問程式中的任何物件和函式
- 具有自動和執行緒區域性儲存期的物件仍可以透過指標或引用被其他執行緒訪問。
- 在宿主實現下,C++ 程式可以有多個執行緒併發執行。每個執行緒的執行按本頁其餘部分定義進行。整個程式的執行由其所有執行緒的執行組成。
- 在獨立實現下,程式是否可以有多個執行執行緒是實現定義的。
對於並非因呼叫 std::raise 而執行的訊號處理程式,包含訊號處理程式呼叫的執行執行緒是未指定的。
目錄 |
[編輯] 資料競爭
不同的執行執行緒總是被允許併發訪問(讀取和修改)不同的記憶體位置,沒有干擾,也沒有同步要求。
如果兩個表示式求值中,一個修改了記憶體位置或開始/結束了記憶體位置中物件的生命週期,而另一個讀取或修改了相同的記憶體位置或開始/結束了佔據該記憶體位置儲存的物件的生命週期,則這兩個表示式求值就 衝突。
一個程式,如果存在兩個衝突的求值,則存在 資料競爭,除非:
- 兩個求值都在同一執行緒或同一訊號處理程式中執行,或者
- 兩個衝突的求值都是原子操作(參見 std::atomic),或者
- 其中一個衝突求值 happens-before 另一個(參見 std::memory_order)。
如果發生資料競爭,程式的行為是未定義的。
(特別是,std::mutex 的釋放 synchronized-with(同步於),因此 happens-before(先行於)另一個執行緒對同一互斥體的獲取,這使得可以使用互斥鎖來防止資料競爭。)
int cnt = 0; auto f = [&] { cnt++; }; std::thread t1{f}, t2{f}, t3{f}; // undefined behavior
std::atomic<int> cnt{0}; auto f = [&] { cnt++; }; std::thread t1{f}, t2{f}, t3{f}; // OK
[編輯] 容器資料競爭
標準庫中除了std
::vector<bool> 之外的所有容器都保證在同一容器中對不同元素所包含物件內容的併發修改絕不會導致資料競爭。
std::vector<int> vec = {1, 2, 3, 4}; auto f = [&](int index) { vec[index] = 5; }; std::thread t1{f, 0}, t2{f, 1}; // OK std::thread t3{f, 2}, t4{f, 2}; // undefined behavior
std::vector<bool> vec = {false, false}; auto f = [&](int index) { vec[index] = true; }; std::thread t1{f, 0}, t2{f, 1}; // undefined behavior
[編輯] 記憶體順序
當一個執行緒從記憶體位置讀取一個值時,它可能會看到初始值、在同一執行緒中寫入的值,或在另一個執行緒中寫入的值。有關從執行緒進行的寫入對其他執行緒可見的順序的詳細資訊,請參閱 std::memory_order。
[編輯] 向前進展
[編輯] 無阻塞性
當只有一個執行緒(未被標準庫函式阻塞)執行無鎖的原子函式時,該執行保證完成(所有標準庫無鎖操作都具有無阻塞性)。
[編輯] 無鎖性
當一個或多個無鎖原子函式併發執行時,至少有一個函式保證完成(所有標準庫無鎖操作都具有無鎖性 —— 確保它們不會被其他執行緒無限地活鎖,例如透過持續竊取快取行,是實現的工作)。
[編輯] 進展保證
在一個有效的 C++ 程式中,每個執行緒最終都會執行以下操作之一:
- 終止。
- 呼叫 std::this_thread::yield。
- 呼叫庫 I/O 函式。
- 透過 volatile glvalue 進行訪問。
- 執行原子操作或同步操作。
- 繼續執行平凡無限迴圈(見下文)。
如果一個執行緒執行了上述執行步驟之一,在標準庫函式中阻塞,或者呼叫了一個原子無鎖函式但因未阻塞的併發執行緒而未能完成,則稱該執行緒 取得了進展。
這允許編譯器移除、合併和重排所有沒有可觀察行為的迴圈,而無需證明它們最終會終止,因為它可以假定沒有任何執行執行緒可以永遠執行而不執行任何這些可觀察行為。對平凡無限迴圈做出了例外,它們不能被移除或重排。
[編輯] 平凡無限迴圈
一個 平凡空迭代語句 是符合以下形式之一的迭代語句:
while ( 條件 ) ; |
(1) | ||||||||
while ( 條件 ) { } |
(2) | ||||||||
do ; while ( 條件 ) ; |
(3) | ||||||||
do { } while ( 條件 ) ; |
(4) | ||||||||
for ( init-statement 條件 (可選) ; ) ; |
(5) | ||||||||
for ( init-statement 條件 (可選) ; ) { } |
(6) | ||||||||
一個平凡空迭代語句的 控制表示式 是:
一個 平凡無限迴圈 是一個平凡空迭代語句,其轉換後的控制表示式是常量表達式,當顯式常量求值時,其值為 true。
平凡無限迴圈的迴圈體被替換為對函式 std::this_thread::yield 的呼叫。在獨立實現中是否發生此替換是實現定義的。
for (;;); // trivial infinite loop, well defined as of P2809 for (;;) { int x; } // undefined behavior
併發向前進展如果一個執行緒提供 併發向前進展保證,則它在未終止的情況下,將在有限時間內 取得進展(如上定義),無論其他執行緒(如果有的話)是否取得進展。 標準鼓勵但不要求主執行緒和由 std::thread 和 std::jthread(C++20 起) 啟動的執行緒提供併發向前進展保證。 並行向前進展如果一個執行緒提供 並行向前進展保證,則實現不要求確保該執行緒在尚未執行任何執行步驟(I/O、volatile、原子或同步)的情況下最終會取得進展,但一旦該執行緒執行了一個步驟,它就會提供 併發向前進展 保證(此規則描述了執行緒池中以任意順序執行任務的執行緒)。 弱並行向前進展如果一個執行緒提供 弱並行向前進展保證,則它不保證最終會取得進展,無論其他執行緒是否取得進展。 這樣的執行緒仍然可以透過阻塞並委託向前進展保證來確保取得進展:如果執行緒 C++ 標準庫中的並行演算法透過委託向前進展保證來阻塞在未指定的一組庫管理執行緒的完成上。 |
(C++17 起) |
[編輯] 缺陷報告
下列更改行為的缺陷報告追溯地應用於以前出版的 C++ 標準。
缺陷報告 | 應用於 | 釋出時的行為 | 正確的行為 |
---|---|---|---|
CWG 1953 | C++11 | 兩個表示式求值,它們開始/結束生命週期 的重疊儲存物件不衝突 |
它們衝突 |
LWG 2200 | C++11 | 不清楚容器資料競爭 要求是否僅適用於序列容器 |
適用於所有容器 |
P2809R3 | C++11 | 執行“平凡”[1] 無限迴圈的行為是未定義的 |
正確定義了“平凡無限迴圈” 並使行為變為良好定義 |
- ↑ 這裡的“平凡”是指執行無限迴圈永遠不會取得任何進展。