12

次のコード サンプルについて質問があります ( m_valueは揮発性ではなく、すべてのスレッドが別のプロセッサで実行されます)。

void Foo() // executed by thread #1, BEFORE Bar() is executed
{
   Interlocked.Exchange(ref m_value, 1);
}

bool Bar() // executed by thread #2, AFTER Foo() is executed
{
   return m_value == 1;
}

Foo() でInterlocked.Exchangeを使用すると、Bar() が実行されたときに値「1」が表示されることが保証されますか? (値が既にレジスタまたはキャッシュ ラインに存在する場合でも?) または、m_valueの値を読み取る前にメモリ バリアを配置する必要がありますか?

また、(元の質問とは関係ありません)、volatile メンバーを宣言し、 InterlockedXXメソッドへの参照によって渡すことは合法ですか? (コンパイラは参照渡しで揮発性を渡すことについて警告するので、そのような場合は警告を無視する必要がありますか?)

注意してください、私は「物事を行うためのより良い方法」を探しているわけではないので、物事を行うための完全に別の方法を提案する回答を投稿しないでください (「代わりにロックを使用する」など)。 ..

4

7 に答える 7

5

メモリバリアは特に役に立ちません。メモリ操作間の順序を指定します。この場合、各スレッドには 1 つのメモリ操作しかないため、問題はありません。典型的なシナリオの 1 つは、構造体 (メモリ バリア) 内のフィールドに非アトミックに書き込み、構造体のアドレスを他のスレッドに発行することです。Barrier は、構造体メンバーへの書き込みが、そのアドレスを取得する前にすべての CPU によって認識されることを保証します。

本当に必要なのはアトミック操作です。InterlockedXXX 関数、または C# の揮発性変数。Bar での読み取りがアトミックである場合、コンパイラも CPU も、Foo への書き込みの前、または Foo への書き込みの後に、どちらが最初に実行されるかに応じて、値の読み取りを妨げる最適化を行わないことを保証できます。Foo の書き込みが Bar の読み取りの前に発生することを「知っている」と言っているので、Bar は常に true を返します。

Bar の読み取りがアトミックでない場合、部分的に更新された値 (つまり、ガベージ) またはキャッシュされた値 (コンパイラまたは CPU から) を読み取る可能性があり、どちらも Bar が本来あるべき true を返すのを妨げる可能性があります。

最近のほとんどの CPU のギャランティー ワード アラインメント読み取りはアトミックであるため、本当の秘訣は、読み取りがアトミックであることをコンパイラーに伝える必要があることです。

于 2009-11-18T19:13:30.853 に答える
4

メモリ バリアの使用の通常のパターンは、クリティカル セクションの実装に入れるものと一致しますが、プロデューサーとコンシューマーのペアに分割されます。例として、クリティカル セクションの実装は通常、次の形式になります。

while (!pShared->lock.testAndSet_Acquire()) ;
// (このループには、次のような通常のクリティカル セクションのものをすべて含める必要があります
// スピン、廃棄、
// pause() 命令、およびリソースに対する最後の手段であるギブアップ アンド ブロッキング
// ロックが利用可能になるまで。)

// 共有メモリへのアクセス。

pShared->foo = 1
v = pShared-> goo

pShared->lock.clear_Release()

上記のメモリ バリアの取得により、ロックの変更が成功する前に開始された可能性のあるすべてのロード (pShared->goo) が破棄され、必要に応じて再起動されるようになります。

リリース メモリ バリアは、共有メモリを保護するロック ワードがクリアされる前に、goo から (ローカルの) 変数 v へのロードが完了することを保証します。

典型的なプロデューサーとコンシューマーのアトミック フラグ シーンで同様のパターンがあります (それがあなたがしていることであるかどうかをサンプルで判断するのは困難ですが、アイデアを説明する必要があります)。

プロデューサーがアトミック変数を使用して、他の状態を使用する準備ができていることを示したとします。次のようなものが必要です。

pShared->goo = 14

pShared->atomic.setBit_Release()

ここでプロデューサに「書き込み」バリアがないと、goo ストアが CPU ストア キューを通過する前に、ハードウェアがアトミック ストアに到達しないという保証はありません。 (コンパイラが希望どおりに順序付けするメカニズムがある場合でも)。

消費者では

if ( pShared->atomic.compareAndSwap_Acquire(1,1) )
{
   v = pShared->グー
}

ここに「読み取り」バリアがないと、アトミック アクセスが完了する前に、ハードウェアが動作せず、グーを取得していないことがわかりません。アトミック (つまり、ロック cmpxchg のような処理を行うインターロック関数で操作されるメモリ) は、それ自体に関してのみ「アトミック」であり、他のメモリではありません。

さて、言及しなければならない残りのことは、バリア構造は非常に移植性が低いということです。コンパイラは、ほとんどのアトミック操作メソッドに対して _acquire と _release のバリエーションを提供している可能性があり、これらは、それらを使用する種類の方法です。使用しているプラ​​ットフォーム (ia32 など) によっては、これらは _acquire() または _release() サフィックスなしで得られるものとまったく同じである可能性があります。これが問題となるプラットフォームは、ia64 (まだわずかにひきつっている HP を除いて事実上死んでいます) と powerpc です。ia64 には、ほとんどのロードおよびストア命令 (cmpxchg のようなアトミック命令を含む) に .acq および .rel 命令修飾子がありました。powerpc には、これに対する個別の指示があります (isync と lwsync は、それぞれ読み取りバリアと書き込みバリアを提供します)。

今。これをすべて言った。この道を行く正当な理由は本当にありますか?これらすべてを正しく行うことは、非常に困難な場合があります。コード レビューで多くの自己疑念と不安に備え、あらゆる種類のランダムなタイミング シナリオで多数の同時実行テストを行うようにしてください。避けるべき非常に正当な理由がない限り、クリティカル セクションを使用し、そのクリティカル セクションを自分で作成しないでください。

于 2009-11-19T04:43:11.260 に答える
2

完全にはわかりませんが、Interlocked.Exchangeは Windows API の InterlockedExchange 関数を使用して、とにかく完全なメモリ バリアを提供すると思います。

この関数は、完全なメモリ バリア (またはフェンス) を生成して、メモリ操作が順番に完了するようにします。

于 2009-11-18T19:20:09.563 に答える
1

m_valueが としてマークされていない場合volatile、読み込まれた値が保護されていると考える理由はありませんBar。コンパイラの最適化、キャッシュ、またはその他の要因により、読み取りと書き込みの順序が変更される可能性があります。インターロック交換は、適切にフェンスされたメモリ参照のエコシステムで使用される場合にのみ役立ちます。これは、フィールドをマークすることの要点ですvolatile。.Net メモリ モデルは、一部の人が期待するほど単純ではありません。

于 2009-11-19T04:55:18.427 に答える
1

連動交換操作により、メモリ バリアが保証されます。

次の同期関数は、適切なバリアを使用してメモリの順序を保証します。

  • クリティカル セクションに出入りする関数

  • 同期オブジェクトを通知する関数

  • 待機関数

  • 連動機能

(出典:リンク

しかし、レジスタ変数には不運です。m_value が Bar のレジスタにある場合、m_value への変更は表示されません。このため、共有変数を 'volatile' と宣言する必要があります。

于 2009-11-18T18:38:33.467 に答える
0

Interlocked.Exchange() は、値がすべての CPU に適切にフラッシュされることを保証する必要があります。独自のメモリ バリアを提供します。

コンパイラが volatile を Interlocked.Exchange() に渡すことについて不平を言っていることに驚いています。Interlocked.Exchange() を使用しているという事実は、volatile 変数をほぼ強制する必要があります。

発生する可能性のある問題は、コンパイラが Bar() のいくつかの重度の最適化を実行し、m_value の値を変更するものが何もないことに気付いた場合、チェックを最適化できなくなることです。それが volatile キーワードが行うことです。これは、その変数がオプティマイザのビューの外で変更される可能性があることをコンパイラに示唆します。

于 2009-11-18T18:33:27.130 に答える
0

Bar() の前に読み取ってはならないことをコンパイラまたはランタイムに伝えないm_value場合、値をm_value先にキャッシュしBar()て、キャッシュされた値を単に使用することができます。の「最新」バージョンが確実に認識されるようにするには、 をm_value押し込むか、 をThread.MemoryBarrier()使用しますThread.VolatileRead(ref m_value)。後者は、フル メモリ バリアよりも安価です。

理想的には、ReadBarrier を押し込むことができますが、CLR はそれを直接サポートしていないようです。

EDIT:それについて考える別の方法は、実際には2種類のメモリバリアがあるということです。コンパイラに読み取りと書き込みの順序付け方法を伝えるコンパイラメモリバリアと、CPUに読み取りと書き込みの順序付け方法を伝えるCPUメモリバリアです。関数は、InterlockedCPU メモリ バリアを使用します。コンパイラがそれらをコンパイラ メモリ バリアとして扱ったとしても、この特定のケースでBar()は個別にコンパイルされm_value、コンパイラ メモリ バリアを必要とする他の用途が知られていない可能性があるため、問題にはなりません。

于 2009-11-18T22:18:28.653 に答える