19

ループの実行を制御するフィールドがあると仮定します。

private static bool shouldRun = true;

そして、次のようなコードを持つスレッドを実行しています。

while(shouldRun) 
{
    // Do some work ....
    Thread.MemoryBarrier();
}

これで、同期メカニズムを使用せずに、別のスレッドがに設定さshouldRunれる可能性があります。false

Thread.MemoryBarrier()を理解している限り、whileループ内にこの呼び出しを含めると、作業スレッドがキャッシュされたバージョンのを取得できなくなりshouldRun、無限ループが発生するのを効果的に防ぐことができます。

Thread.MemoryBarrierについての私の理解は正しいですか?変数を設定できるスレッドがある場合(これは簡単に変更できません)、これは、スレッドによってfalseに設定されるshouldRunと、ループが確実に停止するようにするための合理的な方法ですか?shouldRun

4

5 に答える 5

26

これはThread.MemoryBarrier()の正しい使用法ですか?

いいえ。ループの実行が開始される前に、1つのスレッドがフラグを設定するとします。フラグのキャッシュされた値を使用して、ループを1回実行することもできます。それは正しいですか?それは確かに私には間違っているようです。ループを最初に実行する前にフラグを設定すると、ループは1回ではなく、0回実行されると思います。

Thread.MemoryBarrier()を理解している限り、whileループ内でこの呼び出しを行うと、作業スレッドがキャッシュされたバージョンのshouldRunを取得できなくなり、無限ループが発生するのを効果的に防ぐことができます。Thread.MemoryBarrierについての私の理解は正しいですか?

メモリバリアは、プロセッサが読み取りと書き込みの並べ替えを行わないことを保証します。これにより、論理的にバリアの前にあるメモリアクセスが実際にバリアのにあることが観察され、その逆も同様です。

ローロックコードを実行することにひどい思いをしているのであれば、明示的なメモリバリアを導入するのではなく、フィールドを揮発性にする傾向があります。「揮発性」C#言語の機能です。危険でよく理解されていない機能ですが、言語の機能です。これは、問題のフィールドが複数のスレッドをロックせずに使用されることをコードのリーダーに明確に伝えます。

これは、スレッドによってshouldRunがfalseに設定されたときにループが停止することを保証するための合理的な方法ですか?

一部の人々はそれを合理的だと考えるでしょう。非常に正当な理由がなければ、自分のコードでこれを行うことはありません。

通常、ローロック手法はパフォーマンスの考慮事項によって正当化されます。そのような考慮事項は2つあります。

まず、競合するロックは潜在的に非常に遅いです。ロック内で実行されているコードがある限り、ブロックされます。競合が多すぎるためにパフォーマンスの問題が発生した場合は、最初に競合を排除して問題を解決しようとします。競合を排除できなかった場合にのみ、ローロック手法を採用します。

第二に、競合していないロックが遅すぎる可能性があります。ループで実行している「作業」にかかる時間がたとえば200ナノ秒未満の場合、競合のないロックをチェックするために必要な時間(約20 ns)は、作業に費やされる時間のかなりの部分です。その場合、ループごとにより多くの作業を行うことをお勧めします。制御フラグが設定されてから200ns以内にループが停止することが本当に必要ですか?

最も極端なパフォーマンスシナリオでのみ、競合のないロックをチェックするコストがプログラムで費やされる時間のかなりの部分であると想像します。

また、もちろん、200 ns程度ごとにメモリバリアを誘発している場合は、他の方法でパフォーマンスを低下させている可能性もあります。プロセッサは、これらの移動メモリアクセスを時間内に最適化することを望んでいます。あなたがそれらの最適化を絶えず放棄することを強制しているなら、あなたは潜在的な勝利を逃しています。

于 2012-01-04T16:34:10.137 に答える
7

私はあなたの理解がこの線で少しずれていると思います

Thread.MemoryBarrier()を理解している限り、whileループ内でこの呼び出しを行うと、作業スレッドがキャッシュされたバージョンのshouldRunを取得できなくなり、無限ループが発生するのを効果的に防ぐことができます。

メモリバリアは、読み取り/書き込み命令に順序の制約を適用する方法です。読み取り/書き込みの並べ替えの結果は、メモリバリアをキャッシュしているように見える場合がありますが、実際にはキャッシュに影響を与えることはありません。これは、読み取りと書き込みの命令が交差できないフェンスとして機能するだけです。

おそらく、これは無限ループを防ぐことはできません。メモリフェンスが行っているのは、このシナリオでは、ループの本体で発生するすべての読み取りと書き込みが、shouldRun条件付きのループによって値が読み取られる前に発生するように強制することです。

ウィキペディアには、メモリバリアに関する優れたウォークスルーがあります。

于 2012-01-04T15:56:28.810 に答える
3

あなたがやろうとしていることに応じて、これはおそらくあなたが期待することをしないでしょう。MSDNから、MemoryBarrier:

メモリアクセスを次のように同期します。現在のスレッドを実行しているプロセッサは、MemoryBarrierの呼び出しに続くメモリアクセスの後にMemoryBarrierの呼び出しの前にメモリアクセスが実行されるように命令を並べ替えることはできません。

これにより、メモリバリア呼び出し後の命令の並べ替えが防止されますが、スレッドスケジューリングでは、ライタースレッドが実際に書き込みを実行する前に、ループをもう一度実行する必要があると判断することはできません。たとえば、ロックや同期のメカニズムではありません。これは、shouldRunの値をチェックする に、ループ内の他の命令(変数の初期化など)が並べ替えられるのを防ぐだけです。

同様に、これを省略しても無限ループは発生しません。どちらの場合も、shouldRunは反復ごとにチェックされます。ここには「キャッシュされた値」はありません。

于 2012-01-04T15:50:22.513 に答える
3

これは質問に対する直接の答えではありません。ただし、それは上記の投稿で十分にカバーされているようです。明確に述べられていないように思われるのは、望ましい結果を達成する方法です。意図したスレッド同期オブジェクトを使用するだけでも、実際には何も問題はありません。

private static readonly ManualResetEvent shutdownEvent = new ManualResetEvent(false);

//Thread 1 - continue until event is signaled
while(!shutodwnEvent.WaitOne(0)) 
{
    // Do some work ....
    Thread.MemoryBarrier();
}

//Thread 2 - signal the other thread
shutdownEvent.Set();

もう1つのアプローチは、変数にアクセスするときにlockステートメントを使用することです。

private static object syncRoot = new Object();
private static bool shouldRun = true;

//Thread 1
bool bContinue;
lock(syncRoot)
    bContinue = shouldRun;

while(bContinue) 
{
    // Do some work ....
    lock(syncRoot)
        bContinue = shouldRun;
}

//Thread 2
lock(syncRoot)
    shouldRun = false;
于 2012-01-04T17:27:33.547 に答える
2

この場合Thread.MemoryBarrier();、パフォーマンスは低下しますが、安全性は低下しません。

private static volatile readonly bool shouldRun = true;

これは、最新の読み取りを実現するために必要な場合、shouldRunをvolatileとして宣言するとメモリバリアが実行されるためですが、そうする必要はない場合があります。

于 2012-01-04T15:44:25.253 に答える