C11 基準の裁定は次のとおりです。
5.1.2.4 マルチスレッド実行とデータ競合
次の場合、評価 A は評価 B よりも前に依存順序付けされ ます。
— A がアトミック オブジェクト M に対して解放操作を実行し、別のスレッドで B が M に対して消費操作を実行し、 A が先頭にある解放シーケンスの副作用によって書き込まれた値を読み取る、または
— 一部の評価 X では、A は X の前に依存関係の順序で並べられ、X は B への依存関係を持ちます。
A が B と同期している場合、A が B の前に依存順序付けされている場合、または評価 X の場合、評価Aがスレッド間で評価 B の前に発生します。
— A は X と同期し、X は B の前にシーケンスされます。
— A が X の前にシーケンスされ、X が B の前にスレッド間で発生する、または
— X の前にスレッド間が発生し、B の前に X のスレッド間が発生します。
注 7 「スレッド間発生」関係は、2 つの例外を除いて、「前に配列」、「同期」、および「前に依存順序付け」関係の任意の連結を記述します。最初の例外は、連結の最後に「前に順序付けられた依存関係」があり、その後に「前に順序付けられた」が続くことは許可されていないことです。この制限の理由は、「前に依存順序付け」関係に参加する消費操作は、この消費操作が実際に依存関係を持つ操作に関してのみ順序付けを提供するためです。この制限がそのような連結の最後にのみ適用される理由は、後続の解放操作によって、前の消費操作に必要な順序が提供されるためです。2 番目の例外は、連結が「前に配列」だけで構成されることは許可されていないことです。この制限の理由は、(1) 「スレッド間発生前」を推移的に閉じることを許可するため、(2) 以下で定義される「前に発生」関係は、「前に順序付けられた」から完全に構成される関係を提供するためです。 」。
AがB の前にシーケンスされている場合、またはスレッド間でA が B の前に発生している場合、評価 A は評価 B の前に発生します。
M の値計算 B に関して、オブジェクト M に対する目に見える副作用 Aは、次の条件を満たします。
— A が B の前に発生し、かつ
— A が X の前に発生し、X が B の前に発生するような、X から M への他の副作用はありません。
評価 B によって決定される非アトミック スカラー オブジェクト M の値は、目に見える副作用 A によって格納される値でなければなりません。
(強調追加)
以下の解説では、以下のように略記します。
- 前に順序付けられた依存関係: DOB
- スレッド間の発生前: ITHB
- 以前の出来事: HB
- 前に配列決定: SeqB
これがどのように適用されるかを見てみましょう。関連する 4 つのメモリ操作があり、評価 A、B、C、D と名付けます。
スレッド 1:
y.store (20); // Release; Evaluation A
x.store (10); // Release; Evaluation B
スレッド 2:
if (x.load() == 10) { // Consume; Evaluation C
assert (y.load() == 20) // Consume; Evaluation D
y.store (10)
}
assert がトリップしないことを証明するために、実際には、A が D で常に目に見える副作用であることを証明しようとします。5.1.2.4 (15)に従って、次のようになります。
A SeqB B DOB C SeqB D
これは、DOB で終わり、その後に SeqB が続く連結です。これは、(16) が述べていることにもかかわらず、(17) によってITHB 連結ではないことが明示的に規定されています。
A と D は同じ実行スレッドにないため、A は SeqB D ではありません。したがって、HB の (18) の 2 つの条件はいずれも満たされておらず、A は HB D を満たしていません。
したがって、(19) の条件の 1 つが満たされないため、A は D には見えません。アサートが失敗する場合があります。
これがどのように展開されるかについては、ここ、C++ 標準のメモリ モデルの説明、およびセクション 4.2 コントロールの依存関係で説明されています。
- (しばらくしてから) スレッド 2 の分岐予測子は、
if
が実行されると推測します。
- スレッド 2 は予測された分岐に近づき、投機的フェッチを開始します。
- スレッド 2 が順不同で投機的に(評価 D)
0xGUNK
からロードされます。y
(キャッシュからまだ追い出されていないのでしょうか?)。
- スレッド 1 ストア
20
にy
(評価 A)
10
スレッド1 店舗x
(評価 B)
- スレッド 2 ロード
10
(x
評価 C)
- スレッド 2 は、取得されたことを確認し
if
ます。
- スレッド 2 の投機的ロード
y == 0xGUNK
がコミットされます。
- スレッド 2 はアサートに失敗します。
評価 D が C の前に並べ替えられることが許可される理由は、consumeがそれを禁止していないためです。これは、プログラム順でそれ以降のロード/ストアがその前に並べ替えられるのを防ぐacquire-loadとは異なります。繰り返しますが、5.1.2.4(15) は、「前に依存関係を順序付けた」関係に参加する消費操作は、この消費操作が実際に依存関係を運ぶ操作に関してのみ順序付けを提供し、依存関係はほとんど確実に存在しないと述べています。 2 つの負荷の間。
CppMem 検証
CppMemは、C11 および C++11 メモリ モデルでの共有データ アクセス シナリオの調査に役立つツールです。
質問のシナリオに近い次のコードの場合:
int main() {
atomic_int x, y;
y.store(30, mo_seq_cst);
{{{ { y.store(20, mo_release);
x.store(10, mo_release); }
||| { r3 = x.load(mo_consume).readsvalue(10);
r4 = y.load(mo_consume); }
}}};
return 0; }
このツールは、次の2 つの一貫した競合のないシナリオを報告します。

がy=20
正常に読み取られ、

「古い」初期化値y=30
が読み取られます。フリーハンドの円は私のものです。
対照的にmo_acquire
、ロードに を使用すると、CppMem は一貫性のある競合のないシナリオ、つまり正しいシナリオを1 つだけ報告します。

がy=20
読み取られます。