75

私は章を読みましたが、あまり好きではありませんでした。各メモリの順序の違いはまだわかりません。これは、はるかに単純なhttp://en.cppreference.com/w/cpp/atomic/memory_orderを読んだ後に理解した私の現在の推測です。

以下は間違っているので、そこから学ぼうとしないでください

  • memory_order_relaxed:同期しませんが、別のアトミック変数の別のモードから注文が行われた場合は無視されません
  • memory_order_consume:このアトミック変数の読み取りを同期しますが、これより前に書き込まれた緩和された変数は同期しません。ただし、スレッドがYを変更するときにvar Xを使用する(そしてそれを解放する)場合。Yを消費する他のスレッドでは、Xもリリースされますか?これがこのスレッドがx(そして明らかにy)の変更をプッシュすることを意味するかどうかはわかりません
  • memory_order_acquire:このアトミック変数の読み取りを同期し、これが同期される前に書き込まれた緩和された変数を確認します。(これは、すべてのスレッドのすべてのアトミック変数が同期されることを意味しますか?)
  • memory_order_release:アトミックストアを他のスレッドにプッシュします(ただし、consum / acquireで変数を読み取る場合のみ)
  • memory_order_acq_rel:読み取り/書き込み操作用。古い値を変更せず、変更を解放するように取得します。
  • memory_order_seq_cst:更新を他のスレッドで強制的に表示することを除いて、acquire releaseと同じです(a別のスレッドでrelaxedで保存する場合。seq_cstで保存bします。relaxで3番目のスレッドを読み取ると、他のアトミック変数a とともに変更が表示されますか? b)。

私は理解したと思いますが、間違っている場合は訂正してください。読みやすい英語でそれを説明するものは何も見つかりませんでした。

4

3 に答える 3

87

GCC Wikiは、コード例を使用して非常に徹底的で理解しやすい説明を提供します。

(抜粋を編集し、強調を追加)

重要:

答えに私自身の言葉遣いを追加する過程でGCCWikiからコピーされた以下の引用を読み直すと、引用が実際には間違っていることに気づきました。彼らは取得し、まったく間違った方法で消費しました。release-consume操作は、依存データの順序付け保証のみを提供しますが、release -acquire操作は、データがアトミック値に依存しているかどうかに関係なく、その保証を提供します。

最初のモデルは「逐次一貫性」です。これは、何も指定されていない場合に使用されるデフォルトのモードであり、最も制限が厳しいモードです。を介して明示的に指定することもできますmemory_order_seq_cst。これは、スレッド全体に適用されることを除いて、シーケンシャルプログラマーが本質的に精通しているのと同じ制限と負荷の移動に対する制限を提供します。
[...]
実用的な観点から、これは最適化の障壁として機能するすべての不可分操作に相当します。アトミック操作間で物事を並べ替えることは問題ありませんが、操作全体ではできません。他のスレッドへの可視性がないため、スレッドローカルのものも影響を受けません。[...]このモードでは、すべてのスレッドで一貫性も提供されます。

反対のアプローチはですmemory_order_relaxed。このモデルでは、発生前の制限を削除することで、同期を大幅に減らすことができます。これらのタイプのアトミック操作では、デッドストアの削除や共通化など、さまざまな最適化を実行することもできます。[...]何も起こらず、エッジの前では、スレッドは別のスレッドからの特定の順序を当てにすることはできません。
リラックスモードは、プログラマーが他の共有メモリデータのスレッドを同期するために使用するのではなく、変数を本質的にアトミックにしたい場合に最も一般的に使用されます。

3番目のモード(memory_order_acquire/ memory_order_release)は、他の2つのモードのハイブリッドです。取得/解放モードは、従属変数に発生前の関係のみを適用することを除いて、逐次一貫性モードに似ています。これにより、独立した書き込みの独立した読み取り間に必要な同期を緩和できます。

memory_order_consumeは、リリース/取得メモリモデルのさらに微妙な改良であり、非依存の共有変数で順序付けする前に発生を削除することで、要件をわずかに緩和します。
[...]
本当の違いは、同期するためにハードウェアがフラッシュしなければならない状態の量に要約されます。したがって、消費操作より高速に実行される可能性があるため、実行内容を知っている人は、パフォーマンスが重要なアプリケーションにそれを使用できます。

これは、もっとありふれた説明での私自身の試みに続くものです。

それを調べる別のアプローチは、アトミックと通常の両方の読み取りと書き込みの並べ替えの観点から問題を調べることです。

すべてのアトミック操作は、それ自体がアトミックであり(2つのアトミック操作の組み合わせは全体としてアトミックではありません!)、実行ストリームのタイムラインに表示される順序全体で表示されることが保証されています。つまり、どのような状況でも、アトミック操作を並べ替えることはできませんが、他のメモリ操作は並べ替えることができます。コンパイラ(およびCPU)は、最適化などの並べ替えを定期的に実行します。
また、コンパイラは、いつでも実行されるアトミック操作が、以前に実行された他のすべてのアトミック操作(おそらく他のプロセッサコア(必ずしも他の操作ではない))の結果を確認するために必要な命令を使用する必要があることも意味します。 。

さて、リラックスはまさにそれであり、最低限です。さらに何もせず、他の保証も提供しません。可能な限り安価な操作です。強く順序付けられたプロセッサアーキテクチャ(x86 / amd64など)での非読み取り-変更-書き込み操作の場合、これは単純な通常の通常の動きに要約されます。

逐次一貫性のある操作は正反対であり、アトミック操作だけでなく、前後に発生する他のメモリ操作に対しても厳密な順序付けを強制します。どちらも不可分操作によって課せられた障壁を越えることはできません。実際には、これは最適化の機会が失われることを意味し、場合によってはフェンス命令を挿入する必要があります。これは最も高価なモデルです。

リリース操作は、通常のロードとストアがアトミック操作の後に並べ替えられるのを防ぎますが、取得操作は、通常のロードとストアがアトミック操作のに並べ替えられるのを防ぎます。他のすべてはまだ移動することができます。
ストアが後に移動されるのを防ぎ、ロードがそれぞれのアトミック操作の前に移動されるのを防ぐことの組み合わせにより、取得スレッドが見るものはすべて一貫しており、最適化の機会が少しだけ失われます。
これは、(ライターによって)解放され(リーダーによって)取得されている、存在しないロックのようなものと考えることができます。ただし...ロックはありません。

実際には、リリース/取得は通常、コンパイラが特に高価な特別な命令を使用する必要がないことを意味しますが、ロードとストアを好みに合わせて自由に並べ替えることができず、いくつかの(小さな)最適化の機会を逃す可能性があります。

最後に、consumeはacquireと同じ操作ですが、順序の保証が依存データにのみ適用される点が異なります。依存データは、たとえば、アトミックに変更されたポインターによってポイントされるデータになります。
おそらく、それは取得操作には存在しないいくつかの最適化の機会を提供する可能性があります(制限の対象となるデータが少ないため)が、これはより複雑でエラーが発生しやすいコードと重要なタスクを犠牲にして発生します依存関係チェーンを正しく取得する方法。

現在、仕様が改訂されている間は、消費順序を使用することはお勧めしません。

于 2012-12-30T15:12:47.573 に答える
39

これは非常に複雑な問題です。http://en.cppreference.com/w/cpp/atomic/memory_orderを数回読んだり、他のリソースを読んだりしてみてください。

簡単な説明は次のとおりです。

コンパイラCPUは、メモリアクセスを並べ替えることができます。つまり、コードで指定されている順序とは異なる順序で発生する可能性があります。ほとんどの場合、これで問題ありません。別のスレッドが通信を試みたときに問題が発生し、コードの不変条件を壊すようなメモリアクセスの順序が表示される場合があります。

通常、同期にはロックを使用できます。問題は彼らが遅いということです。同期はCPUレベルで行われるため、アトミック操作ははるかに高速です(つまり、CPUは、別のCPU上であっても、他のスレッドが変数を変更しないようにします)。

したがって、私たちが直面している1つの問題は、メモリアクセスの並べ替えです。memory_order列挙型は、コンパイラが禁止する必要のある並べ替えの種類を指定します。

relaxed-制約はありません。

consume-新しくロードされた値に依存するロードは、wrtで並べ替えることができません。アトミックロード。つまり、それらがソースコードのアトミックロードの後である場合、それらはアトミックロードの後でも発生します。

acquire-ロードを並べ替えることはできません。アトミックロード。つまり、それらがソースコードのアトミックロードの後である場合、それらはアトミックロードの後でも発生します。

release-店舗を再注文することはできません。アトミックストア。つまり、ソースコードのアトミックストアの前にある場合は、アトミックストアの前にも発生します。

acq_rel-acquirerelease組み合わせます。

seq_cst-この注文が必要な理由を理解するのはより困難です。基本的に、他のすべての順序付けは、同じアトミック変数を消費/解放するスレッドに対してのみ、特定の許可されていない再順序付けが発生しないことを保証するだけです。メモリアクセスは、他のスレッドに任意の順序で伝播できます。この順序付けにより、これが発生しないことが保証されます(したがって、逐次一貫性)。これが必要な場合は、リンク先ページの最後にある例を参照してください。

于 2012-09-10T12:01:07.580 に答える
3

他の回答は、さまざまな種類の不可分操作に関連してどの操作を並べ替えることができるか、またはできないかを説明していますが、代わりに、より高レベルの説明を提供したいと思います。


無視すること:

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_releasememory_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と同期します。acquireacq_relreleaseacq_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の効果を組み合わせたものです。acquirerelease


他の標準ライブラリ機能との類似点:

いくつかの標準ライブラリ機能も、リレーションシップと同様の同期を引き起こします。たとえば、ミューテックスのロックは、ロックが取得操作であり、ロック解除が解放操作であるかのように、最新のロック解除と同期します。


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本の糸だけが体に入ることができるようにしたいとしましょうifseq_cstあなたがそれを行うことができます。ここでは、取得/リリースまたは弱い注文では不十分です。


フェンス: atomic_thread_fence(seq_cst);フェンスが行うすべてのことacq_relを実行し、さらに追加機能を実行します。

まあ言ってみれば:

  • スレッド1は、using seq_cstorderを使用して変数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つのスレッドのみが実行されているかのように動作し、スレッドは予期せずインターリーブされます。

于 2022-01-04T22:32:53.133 に答える