マルチスレッド実行とデータ競合 (C++11 以降)
実行スレッドは、特定のトップレベル関数 ( std::thread, std::async, std::jthread(C++20 以降) またはその他の手段によって) の呼び出しから始まり、そのスレッドによって後に実行されるすべての関数呼び出しを再帰的に含む、プログラム内の制御の流れです。
- あるスレッドが別のスレッドを作成するとき、新しいスレッドのトップレベル関数への最初の呼び出しは、作成元のスレッドではなく、新しいスレッドによって実行されます。
任意のスレッドは、プログラム内の任意のオブジェクトおよび関数にアクセスする可能性があります。
- 自動記憶域期間およびスレッドローカル記憶域期間を持つオブジェクトは、ポインタまたは参照を介して別のスレッドからアクセスされる可能性があります。
- ホスト環境実装では、C++プログラムは複数のスレッドを並行して実行できます。各スレッドの実行は、このページの残りの部分で定義されているように進行します。プログラム全体の実行は、すべてのスレッドの実行で構成されます。
- フリースタンディング環境実装では、プログラムが複数の実行スレッドを持つことができるかどうかは実装定義です。
std::raise の呼び出しの結果として実行されないシグナルハンドラの場合、シグナルハンドラの呼び出しを含む実行スレッドは未指定です。
目次 |
[編集] データ競合
異なる実行スレッドは、干渉や同期要件なしに、常に異なるメモリーロケーションに並行してアクセス (読み取りおよび変更) することが許可されます。
2つの式評価は、一方の評価がメモリーロケーションを変更するか、メモリーロケーション内のオブジェクトのライフタイムを開始/終了し、もう一方の評価が同じメモリーロケーションを読み取るか変更するか、またはメモリーロケーションと重なるストレージを占めるオブジェクトのライフタイムを開始/終了する場合に競合します。
2つの競合する評価を持つプログラムは、以下の場合を除いてデータ競合を起こします。
- 両方の評価が同じスレッドまたは同じシグナルハンドラで実行される場合、または
- 両方の競合する評価がアトミック操作である場合 (std::atomic を参照)、または
- 競合する評価の1つが別の評価にhappens-beforeする場合 (std::memory_order を参照)。
データ競合が発生した場合、プログラムの動作は未定義です。
(特に、std::mutex の解放は、別のスレッドによる同じミューテックスの取得と同期され、したがって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 は、スレッドから行われた書き込みが他のスレッドに可視になる順序の詳細を説明しています。
[編集] 前方進行
[編集] 妨害フリー
標準ライブラリ関数でブロックされていないスレッドが、ロックフリーであるアトミック関数を実行する場合、その実行は完了することが保証されます (すべての標準ライブラリのロックフリー操作は妨害フリーです)。
[編集] ロックフリー
1つ以上のロックフリーアトミック関数が同時に実行される場合、それらのうち少なくとも1つが完了することが保証されます (すべての標準ライブラリのロックフリー操作はロックフリーです。キャッシュラインを継続的に奪うなど、他のスレッドによって無期限にライブロックされないことを保証するのは実装の役割です)。
[編集] 進行保証
有効な C++ プログラムでは、すべてのスレッドはいずれ以下のいずれかの動作を行います。
- 終了する。
- std::this_thread::yield を呼び出す。
- ライブラリの I/O 関数を呼び出す。
- volatile glvalue を介してアクセスを行う。
- アトミック操作または同期操作を実行する。
- 自明な無限ループ (下記参照) の実行を継続する。
スレッドが上記の実行ステップのいずれかを実行するか、標準ライブラリ関数でブロックするか、ブロックされていない並行スレッドのために完了しないアトミックなロックフリー関数を呼び出す場合、そのスレッドは進行していると言われます。
これにより、コンパイラは、観測可能な動作がないすべてのループを、それが最終的に終了することを証明する必要なく、削除、マージ、および並べ替えることができます。これは、どの実行スレッドも、これらの観測可能な動作のいずれかを実行することなく、永遠に実行できないと仮定できるためです。自明な無限ループについては、削除も並べ替えもできないため、配慮がなされます。
[編集] 自明な無限ループ
自明に空の反復ステートメントは、以下のいずれかの形式に一致する反復ステートメントです。
while ( condition ); |
(1) | ||||||||
while ( condition ) { } |
(2) | ||||||||
do ; while ( condition ); |
(3) | ||||||||
do { } while ( condition ); |
(4) | ||||||||
for ( init-statement condition (オプション) ; ); |
(5) | ||||||||
for ( init-statement condition (オプション) ; ) { } |
(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、atomic、または同期) を実行していない場合、実装はそのスレッドが最終的に進行することを保証する必要はありませんが、一度このスレッドがステップを実行すると、並行前方進行保証を提供します (このルールは、任意の順序でタスクを実行するスレッドプール内のスレッドを記述します)。 弱並列前方進行スレッドが弱並列前方進行保証を提供する場合は、他のスレッドが進行しているかどうかにかかわらず、最終的に進行することを保証しません。 そのようなスレッドは、前方進行保証委譲によるブロックによって依然として進行が保証されえます。つまり、スレッド `P` が一連のスレッド `S` の完了時にこのようにブロックする場合、`S` 内の少なくとも1つのスレッドは `P` と同等またはより強力な前方進行保証を提供します。そのスレッドが完了すると、`S` 内の別のスレッドが同様に強化されます。セットが空になると、`P` はブロック解除されます。 C++ 標準ライブラリの並列アルゴリズムは、未指定のライブラリ管理スレッドの完了時に前方進行委譲でブロックします。 |
(C++17以降) |
[編集] 欠陥報告
以下の動作変更を伴う欠陥報告が、以前に公開されたC++標準に遡って適用されました。
| DR | 適用対象 | 公開された動作 | 正しい動作 |
|---|---|---|---|
| CWG 1953 | C++11 | ライフタイムが重なるオブジェクトのライフタイムを開始/終了する2つの式評価 競合していなかった |
競合する |
| LWG 2200 | C++11 | コンテナのデータ競合要件が シーケンスコンテナにのみ適用されるか不明確だった |
すべてのコンテナに適用される |
| P2809R3 | C++11 | 「自明な」[1]無限ループを実行する動作は 未定義だった |
「自明な無限ループ」を適切に定義し、 動作を明確に定義した |
- ↑ ここでの「自明な」とは、無限ループを実行しても全く進行しないことを意味します。