Multi-threaded executions and data races (since C++11)
実行スレッドは、プログラム内の制御フローであり、特定のトップレベル関数の呼び出し( std::thread 、 std::async 、 std::jthread (C++20以降) またはその他の手段による)から始まり、スレッドによってその後実行されるすべての関数呼び出しを再帰的に含みます。
- あるスレッドが別のスレッドを作成する際、新しいスレッドのトップレベル関数への最初の呼び出しは、作成元スレッドではなく、新しいスレッドによって実行されます。
任意のスレッドがプログラム内のあらゆるオブジェクトと関数にアクセスする可能性があります:
- 自動記憶域期間およびスレッドローカル storage duration を持つオブジェクトは、ポインタまたは参照を通じて他のスレッドからアクセスされる可能性があります。
- hosted implementation では、C++プログラムは複数のスレッドを並行して実行できます。各スレッドの実行は、このページの残りの部分で定義される通りに進行します。プログラム全体の実行は、すべてのスレッドの実行によって構成されます。
- freestanding implementation では、プログラムが複数の実行スレッドを持つことができるかどうかは実装定義です。
signal handler が std::raise の呼び出しの結果として実行されない場合、signal handlerの呼び出しがどの実行スレッドに含まれるかは未規定です。
目次 |
データ競合
異なる実行スレッドは常に、干渉や同期要件なしに、異なる memory locations に同時にアクセス(読み取りおよび変更)することが許可されています。
二つの式の 評価 が競合する のは、一方の式がメモリ位置を変更するか、メモリ位置内のオブジェクトの生存期間を開始/終了し、もう一方の式が同じメモリ位置を読み取りまたは変更するか、そのメモリ位置と重複するストレージを占有するオブジェクトの生存期間を開始/終了する場合である。
2つの競合する評価を持つプログラムは、 data race が存在しない限り、
- 両方の評価が同じスレッド上、または同じ シグナルハンドラ 内で実行される場合、または
- 競合する両方の評価がアトミック操作である場合( std::atomic を参照)、または
- 競合する評価の一方が他方に対して happens-before 関係にある場合( std::memory_order を参照)。
データ競合が発生した場合、プログラムの動作は未定義です。
(特に、 std::mutex の解放は synchronized-with 関係にあり、したがって、別のスレッドによる同じmutexの取得に対して happens-before 関係が成立します。これにより、mutexロックを使用してデータ競合を防ぐことが可能となります。)
int cnt = 0; auto f = [&] { cnt++; }; std::thread t1{f}, t2{f}, t3{f}; // 未定義動作
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}; // 正常 std::thread t3{f, 2}, t4{f, 2}; // 未定義動作
std::vector<bool> vec = {false, false}; auto f = [&](int index) { vec[index] = true; }; std::thread t1{f, 0}, t2{f, 1}; // 未定義動作
メモリ順序
スレッドがメモリ位置から値を読み取る際、初期値、同一スレッドで書き込まれた値、または別のスレッドで書き込まれた値のいずれかを読み取る可能性があります。スレッドからの書き込みが他のスレッドに可視になる順序の詳細については、 std::memory_order を参照してください。
フォワードプログレス
障害自由性
標準ライブラリ関数でブロックされていないスレッドが一つだけ、 atomic function を実行する場合、そのロックフリー操作の実行は完了することが保証されます(すべての標準ライブラリのロックフリー操作は obstruction-free です)。
ロックフリー性
1つ以上のロックフリーなアトミック関数が並行して実行される場合、少なくとも1つは完了することが保証されます(標準ライブラリのすべてのロックフリー操作は lock-free です — 他のスレッドによるキャッシュラインの継続的な横取りなどによって無限にライブロックされないことを保証するのは実装の責務です)。
進行保証
有効なC++プログラムでは、すべてのスレッドは最終的に以下のいずれかを実行します:
- 終了します。
- std::this_thread::yield を呼び出します。
- ライブラリI/O関数を呼び出します。
- volatile glvalueを介してアクセスを実行します。
- アトミック操作または同期操作を実行します。
- 自明な無限ループの実行を継続します(下記参照)。
スレッドは、上記の実行ステップの1つを実行するか、標準ライブラリ関数でブロックするか、または非ブロックされた並行スレッドが原因で完了しないアトミックロックフリー関数を呼び出す場合、 進行する と言われます。
これにより、コンパイラは観測可能な動作を持たないすべてのループを、それらが最終的に終了することを証明する必要なく、削除、統合、および並べ替えることができます。なぜなら、いかなる実行スレッドもこれらの観測可能な動作を実行せずに永遠に実行し続けることはないと仮定できるからです。自明な無限ループに対しては配慮がなされており、それらは削除も並べ替えもできません。
自明な無限ループ
trivially empty iteration statement は、以下のいずれかの形式に一致する反復文です:
while (
条件
) ;
|
(1) | ||||||||
while (
条件
) { }
|
(2) | ||||||||
do ; while (
条件
) ;
|
(3) | ||||||||
do { } while (
条件
) ;
|
(4) | ||||||||
for (
初期化文 条件
(オプション)
; ) ;
|
(5) | ||||||||
for (
初期化文 条件
(オプション)
; ) { }
|
(6) | ||||||||
controlling expression を持つ自明に空の反復文は以下の通りです:
`タグ内のC++キーワード`true`は翻訳せず保持
- 「if present, otherwise」を「が存在する場合、それ以外の場合は」と正確に翻訳
- 元のフォーマットと構造を完全に維持
自明な無限ループ とは、変換された制御式が 定数式 であり、 明示的に定数評価される 場合に true と評価される、自明に空の反復文のことです。
自明な無限ループのループ本体は、関数 std::this_thread::yield の呼び出しに置き換えられます。この置換が freestanding implementations で発生するかどうかは実装定義です。
for (;;); // 自明な無限ループ、P2809により明確に定義済み for (;;) { int x; } // 未定義動作
並行前方進行保証 (Concurrent forward progress)スレッドが 並行前方進行保証 を提供する場合、そのスレッドは(上記で定義された通り)有限時間内に 進行 を達成する。これは、そのスレッドが終了していない限り、他のスレッド(存在する場合)が進行しているかどうかに関係なく適用される。 標準では、メインスレッドおよび std::thread と std::jthread (C++20以降) によって開始されたスレッドが並行前方進行保証を提供することが推奨されているが、必須ではない。 並列前方進行保証 (Parallel forward progress)スレッドが 並列前方進行保証 を提供する場合、実装はそのスレッドがまだ実行ステップ(I/O、volatile、atomic、または同期)を実行していない場合に最終的に進行することを保証する必要はない。しかし、このスレッドがステップを実行した後は、 並行前方進行 保証を提供する(このルールは、タスクを任意の順序で実行するスレッドプール内のスレッドを記述する)。 弱並列前方進行保証 (Weakly parallel forward progress)スレッドが 弱並列前方進行保証 を提供する場合、他のスレッドが進行しているかどうかに関係なく、最終的に進行することを保証しない。
このようなスレッドは、前方進行保証委譲によるブロッキングによって進行が保証される場合がある:スレッド
C++標準ライブラリの 並列アルゴリズム は、ライブラリ管理のスレッドの未指定の集合の完了に対して前方進行委譲を行ってブロックする。 |
(C++17以降) |
不具合報告
以下の動作変更の欠陥報告書は、以前に公開されたC++規格に対して遡及的に適用されました。
| DR | 適用対象 | 公開時の動作 | 正しい動作 |
|---|---|---|---|
| CWG 1953 | C++11 |
ストレージが重複するオブジェクトの生存期間を
開始/終了する2つの式評価が競合しない |
競合する |
| LWG 2200 | C++11 |
コンテナのデータ競合要件がシーケンスコンテナにのみ
適用されるかどうかが不明確 |
すべてのコンテナに適用 |
| P2809R3 | C++11 |
「自明な」無限ループを実行する動作が未定義
[1]
|
「自明な無限ループ」を適切に定義し、
動作を明確に定義 |
- ↑ ここでの「自明」(trivial)とは、無限ループの実行が決して進行しないことを意味します。