他の回答は、さまざまな種類の不可分操作に関連してどの操作を並べ替えることができるか、またはできないかを説明していますが、代わりに、より高レベルの説明を提供したいと思います。
無視すること:
memory_order_consume
-どうやら主要なコンパイラはそれを実装しておらず、彼らは静かにそれをより強力なものに置き換えていmemory_order_acquire
ます。標準自体でさえ、それを避けるように言っています。
メモリオーダーに関するcppreferenceの記事の大部分は「消費」を扱っているので、それを削除すると物事が大幅に簡素化されます。
[[carries_dependency]]
また、やなどの関連機能を無視することもできますstd::kill_dependency
。
データ競合:あるスレッドから非アトミック変数に書き込み、同時に別のスレッドから変数に読み取り/書き込みを行うことをデータ競合と呼び、未定義の動作を引き起こします。
memory_order_relaxed
は最も弱く、おそらく最速のメモリ順序です。
アトミックへの読み取り/書き込みは、データの競合(および後続のUB)を引き起こすことはありません。relaxed
単一の変数に対して、この最小限の保証を提供します。他の変数(アトミックかどうか)を保証するものではありません。
緩和された操作は奇妙な順序でスレッド間を伝播する可能性があり、予測できない(まだ小さな)遅延が変化すると、さまざまな変数への緩和された変更がさまざまなスレッドにさまざまな順序で表示されるようになります。
唯一のルールは次のとおりです。
- 各スレッドは、指示したとおりの順序で各個別の変数にアクセスします。たとえば、逆の順序で
a.store(1, relaxed); a.store(2, relaxed);
書くこと1
はありません。2
ただし、同じスレッド内の異なる変数へのアクセスは、相互に関連して並べ替えることができます。
- スレッドAが変数に数回書き込み、次にスレッドBが数回読み取る場合、同じ順序で値を取得します(ただし、同期しない場合は、もちろん、いくつかの値を数回読み取るか、一部をスキップできます。他の方法でスレッド)。
使用例:アトミック変数を使用して非アトミックデータへのアクセスを同期しようとしないもの:さまざまなカウンター(情報提供のみを目的として存在する)、または他のスレッドに停止を通知する「停止フラグ」。別の例:shared_ptr
参照カウントを内部的にインクリメントするsの操作は、を使用しますrelaxed
。
フェンス: atomic_thread_fence(relaxed);
何もしません。
memory_order_release
、memory_order_acquire
すべてを実行relaxed
し、それ以上のことを実行します(したがって、おそらく低速または同等です)。
を使用できるのはストア(書き込み)のみrelease
です。ロード(読み取り)のみが使用できますacquire
。のようなリードモディファイライト操作fetch_add
は両方(memory_order_acq_rel
)にすることができますが、そうする必要はありません。
これらを使用すると、スレッドを同期できます。
スレッド1がメモリMに対して読み取り/書き込みを行うとしましょう(非アトミック変数またはアトミック変数は関係ありません)。
次に、スレッド1は変数Aへのリリースストアを実行します。その後、スレッド1はそのメモリへのアクセスを停止します。
次に、スレッド2が同じ変数Aの取得ロードを実行する場合、このロードはスレッド1の対応するストアと同期すると言われます。
これで、スレッド2はそのメモリMに対して安全に読み取り/書き込みを行うことができます。
同期するのは最新のライターのみで、先行するライターとは同期しません。
複数のスレッド間で同期を連鎖させることができます。
同期は、メモリの順序に関係なく、任意の数の読み取り-変更-書き込み操作に伝播するという特別なルールがあります。たとえば、スレッド1が実行しa.store(1, release);
、次にスレッド2が実行しa.fetch_add(2, relaxed);
、次にスレッド3が実行しa.load(acquire)
、次にスレッド1がスレッド3と正常に同期する場合、途中でリラックスした操作が行われます。
上記のルールでは、リリース操作X、および同じ変数Xに対する後続の読み取り-変更-書き込み操作(次の非読み取り-変更-書き込み操作で停止)は、Xを先頭とするリリースシーケンスと呼ばれます。取得は、リリースシーケンス内の任意の操作から読み取り、シーケンスの先頭と同期します。)
リードモディファイライト操作が含まれる場合、複数の操作との同期を妨げるものは何もありません。上記の例では、またはfetch_add
を使用している場合、それもスレッド1と同期し、逆に、またはを使用している場合、スレッド3は1に加えて2と同期します。acquire
acq_rel
release
acq_rel
使用例: shared_ptr
。のようなものを使用して参照カウンターをデクリメントしますfetch_sub(1, acq_rel)
。
理由は次のとおりです。スレッド1が読み取り/書き込みを行ってからptr->...
、そのコピーを破棄しptr
、参照カウントをデクリメントするとします。次に、スレッド2は最後に残っているポインターを破棄し、refカウントもデクリメントしてから、デストラクタを実行します。
スレッド2のデストラクタは、以前にスレッド1によってアクセスされたメモリにアクセスするため、acq_rel
同期fetch_sub
が必要です。そうでなければ、データレースとUBが発生します。
フェンス:を使用するatomic_thread_fence
と、基本的に、緩和された不可分操作を解放/取得操作に変えることができます。1つのフェンスを複数の操作に適用したり、条件付きで実行したりできます。
1つ以上の変数からリラックスした読み取り(または他の順序で)を実行atomic_thread_fence(acquire)
し、同じスレッドで実行すると、それらの読み取りはすべて取得操作としてカウントされます。
逆に、を実行しatomic_thread_fence(release)
、その後に任意の数の(場合によっては緩和された)書き込みを行うと、それらの書き込みはリリース操作としてカウントされます。
フェンスは、とフェンスacq_rel
の効果を組み合わせたものです。acquire
release
他の標準ライブラリ機能との類似点:
いくつかの標準ライブラリ機能も、リレーションシップと同様の同期を引き起こします。たとえば、ミューテックスのロックは、ロックが取得操作であり、ロック解除が解放操作であるかのように、最新のロック解除と同期します。
memory_order_seq_cst
すべてを行うacquire
/release
行う、など。これはおそらく最も遅い順序ですが、最も安全な順序でもあります。
seq_cst
読み取りは取得操作としてカウントされます。seq_cst
書き込みはリリース操作としてカウントされます。seq_cst
読み取り-変更-書き込み操作は両方としてカウントされます。
seq_cst
操作は、相互に同期したり、取得/解放操作と同期したりできます。それらを混合することの特殊効果に注意してください(以下を参照)。
seq_cst
はデフォルトの順序です。たとえば、与えられたatomic_int x;
、x = 1;
しx.store(1, seq_cst);
ます。
seq_cst
取得/解放と比較して追加のプロパティがあります。seq_cst
プログラム全体のすべての読み取りと書き込みは、単一のグローバル順序で行われます。
特に、この順序は、同期がseq - cst操作とacquire / release操作(seq- cstとのリリース同期、またはseq-cstはacquireと同期します)。このような混合は、基本的に、影響を受けるseq-cst操作を取得/解放に降格します(seq-cstプロパティの一部を保持している可能性がありますが、信頼しない方がよいでしょう)。
使用例:
atomic_bool x = true;
atomic_bool y = true;
// Thread 1:
x.store(false, seq_cst);
if (y.load(seq_cst)) {...}
// Thread 2:
y.store(false, seq_cst);
if (x.load(seq_cst)) {...}
1本の糸だけが体に入ることができるようにしたいとしましょうif
。seq_cst
あなたがそれを行うことができます。ここでは、取得/リリースまたは弱い注文では不十分です。
フェンス: atomic_thread_fence(seq_cst);
フェンスが行うすべてのことacq_rel
を実行し、さらに追加機能を実行します。
まあ言ってみれば:
- スレッド1は、using
seq_cst
orderを使用して変数Xにアクセスし、次に
- スレッド2スレッドは、任意の順序を使用して同じ変数Xにアクセスし(必ずしも同期を引き起こすとは限りません)、次に
- スレッド2は行い
atomic_thread_fence(seq_cst)
ます。
次に、フェンス後のスレッド2での操作は、スレッド1でのseq-cstアクセスの後に発生します(ただし、スレッド1でのそのseq-cst操作の前の非seq-cst操作は、必ずしもスレッド2でのフェンスの前で発生するとは限りません)。
逆も機能します。
- スレッド1は
atomic_thread_fence(seq_cst)
、
- スレッド1は、任意の順序を使用して変数Xにアクセスし、次に
- スレッド2は、
seq_cst
順序を使用して同じ変数Xにアクセスします。
次に、スレッド1のフェンスの前の操作は、スレッド2のそのseq-cst操作の前に発生しますが、スレッド2の後続の非seq-cst操作の前に発生する必要はありません。
両側にフェンスを設けることもできます。
- スレッド1は
atomic_thread_fence(seq_cst)
、
- スレッド1は、任意の順序を使用して変数Xにアクセスし、次に
- スレッド2は、任意の順序を使用して同じ変数Xにアクセスし、次に
- スレッド2は
atomic_thread_fence(seq_cst)
次に、フェンスの前のスレッド1のすべてが、フェンスの後のスレッド2の何かの前に発生します。
異なる注文間の相互運用
上記を要約すると:
|
relaxed 書きます |
release 書きます |
seq-cst 書きます |
relaxed ロード |
- |
- |
- |
acquire ロード |
- |
と同期します |
と同期します* |
seq-cst ロード |
- |
と同期します* |
と同期します |
* =参加しているseq-cst操作は、混乱したseq-cst順序を取得し、事実上、取得/解放操作に降格されます。これは上で説明されています。
より強力なメモリ順序を使用すると、スレッド間のデータ転送が高速になりますか?
いいえ、そうではないようです。
データレースのないプログラムのシーケンシャルな一貫性
この標準では、プログラムがseq_cst
アクセス(およびミューテックス)のみを使用し、データの競合(UBを引き起こす)がない場合、すべての凝った操作の並べ替えについて考える必要はないと説明されています。プログラムは、一度に1つのスレッドのみが実行されているかのように動作し、スレッドは予期せずインターリーブされます。