15

まず第一に、私はそれがクラスlock{}のための合成砂糖であることを知っています。Monitor(ああ、糖衣構文)

私は単純なマルチスレッドの問題で遊んでいましたが、メモリの任意のWORDをロックすると、他のすべてのメモリがキャッシュされないようにする方法を完全に理解できないことがわかりました。レジスタ/ CPUキャッシュなどです。コードサンプルを使用して、私が言っていることを説明する方が簡単です。

for (int i = 0; i < 100 * 1000 * 1000; ++i) {
    ms_Sum += 1;
}

結局、もちろん予想されるものms_Sumが含まれます。100000000

ここで、同じサイクルを2つの異なるスレッドで実行し、上限を半分にしてエージングします。

for (int i = 0; i < 50 * 1000 * 1000; ++i) {
    ms_Sum += 1;
}

同期がないため、誤った結果が得られます。私の4コアのマシンでは、ほぼ乱数で52 388 219あり、の半分よりわずかに大きくなってい100 000 000ます。で囲むms_Sum += 1;lock {}、当然のことながら、絶対に正しい結果が得られます100 000 000しかし、私にとって興味深いのは(実際には、同じような動作を期待していたと言っている)、lock前の行と後のms_Sum += 1;行を追加すると、答えがほぼ正しくなることです。

for (int i = 0; i < 50 * 1000 * 1000; ++i) {
    lock (ms_Lock) {}; // Note curly brackets

    ms_Sum += 1;
}

この場合、私は通常ms_Sum = 99 999 920、非常に近い値を取得します。

質問:なぜlock(ms_Lock) { ms_Counter += 1; }プログラムを完全に正しくするのにlock(ms_Lock) {}; ms_Counter += 1;ほとんど正しくするのか。任意のms_Lock変数をロックすると、メモリ全体がどのように安定しますか?

どうもありがとう!

PSマルチスレッドに関する本を読みに行きました。

同様の質問

ロックステートメントはどのようにしてプロセッサ内の同期を保証しますか?

スレッドの同期。スレッドを同期するには、このロックだけでは不十分なのはなぜですか

4

5 に答える 5

15

なぜlock(ms_Lock) { ms_Counter += 1; }プログラムを完全に正しくするのに、lock(ms_Lock) {}; ms_Counter += 1;ほとんど正しくするのですか?

良い質問!これを理解するための鍵は、ロックが2つのことを行うことです。

  • これにより、ロックに異議を唱えるスレッドは、ロックを取得できるようになるまで一時停止します。
  • これは、「フルフェンス」とも呼ばれるメモリバリアを引き起こします。

任意のオブジェクトをロックすると、他のメモリがレジスタ/CPUキャッシュなどにキャッシュされないようにする方法が完全にはわかりません。

お気づきのように、レジスタまたはCPUキャッシュにメモリをキャッシュすると、マルチスレッドコードで奇妙なことが起こる可能性があります。(関連トピックの穏やかな説明については、揮発性に関する私の記事を参照してください。)簡単に言うと、別のスレッドがそのメモリを変更する前に、あるスレッドがCPUキャッシュ内のメモリのページのコピーを作成し、最初のスレッドがからの読み取りを行う場合キャッシュ、そして事実上、最初のスレッドが読み取りを時間的に後方に移動しました同様に、メモリへの書き込みは時間的に前に進んでいるように見える場合があります。

メモリバリアは、CPUに「時間の経過とともに移動する読み取りと書き込みがフェンスを越えて移動できないようにするために必要なことを実行する」ように指示する時間のフェンスのようなものです。

興味深い実験は、空のロックの代わりに、そこにThread.MemoryBarrier()を呼び出して、何が起こるかを確認することです。同じ結果が得られますか、それとも異なる結果が得られますか?同じ結果が得られた場合、それが助けになっているのはメモリバリアです。そうしないと、スレッドがほぼ正しく同期されているという事実が、ほとんどのレースを防ぐのに十分な速度でスレッドを遅くしています。

私の推測では、後者だと思います。空のロックはスレッドの速度を十分に低下させているため、競合状態のあるコードにほとんどの時間を費やしていません。強力なメモリモデルプロセッサでは、通常、メモリバリアは必要ありません。(x86マシン、またはItaniumを使用していますか、それとも何ですか?x86マシンには非常に強力なメモリモデルがあり、Itaniumにはメモリバリアを必要とする弱いモデルがあります。)

于 2011-08-20T14:08:49.917 に答える
1

これについてdeafsheepと議論しており、現在のアイデアは次のスキーマとして表すことができます。

ここに画像の説明を入力

時間は左から右に進み、2 つのスレッドは 2 つの行で表されます。

どこ

  • 黒いボックスは、ロックの取得、保持、および解放のプロセスを表します
  • plus は加算操作を表します ( schema は私の PC のスケールを表し、lock には add の約 20 倍の時間がかかります)
  • 白いボックスは、ロックの取得を試行し、さらにロックが使用可能になるまで待機する期間を表します

ブラック ボックスの順序は常にこのようになっています。重複することはできず、常に互いに非常に密接に従う必要があります。その結果、プラスは決して重ならないということは非常に論理的になり、期待される合計に正確に到達する必要があります。

既存のエラーの原因は、この質問で調査されています。

于 2011-08-31T21:30:19.823 に答える
1

使用したスレッドの数はわかりませんが、2 つだと推測しています。4 つのスレッドで実行した場合、ロック解除されたバージョンは、シングルスレッド バージョンの 1/4 にかなり近い結果になると予想されます。 「正しい」結果。

を使用しない場合lock、クアッド プロセッサ マシンは各 CPU にスレッドを割り当て (このステートメントでは、簡単にするために、順番にスケジュールされる他のアプリの存在を考慮していません)、各 CPU に干渉することなくフル スピードで実行されます。他の。各スレッドはメモリから値を取得し、インクリメントしてメモリに戻します。結果はそこにあるものを上書きします。つまり、2 つ (または 3 つ、または 4 つ) のスレッドが同時にフルスピードで実行されているため、他のコアのスレッドによって行われたインクリメントの一部が事実上破棄されます。したがって、最終的な結果は、単一のスレッドから得たものよりも低くなります。

ステートメントを追加するlockと、これは CLR (これは C# のように見えますか?) に、使用可能なコア上の 1 つのスレッドだけがそのコードを実行できるようにするように指示します。これは、上記の状況からの重要な変更です。このコードがスレッドセーフではないことを認識していても (危険に近いだけです)、複数のスレッドが互いに干渉するようになったためです。この不適切なシリアライゼーションにより、(副作用として) 後続のインクリメントが同時に実行される頻度が低くなります。暗黙のロック解除には、このコードとマルチコア CPU の観点から、少なくとも、実行されたすべてのスレッドのウェイクニングが必要になるためです。ロック待ち。このオーバーヘッドのために、このマルチスレッド バージョンはシングルスレッド バージョンよりも遅くなります。スレッドが常にコードを高速化するとは限りません。

待機中のスレッドが待機状態からウェイクアップしている間、ロック解放スレッドはそのタイム スライスで実行を続けることができ、多くの場合、ウェイクアップ スレッドが変数のコピーを取得する機会を得る前に、変数を取得、インクリメント、および保存します。独自のインクリメント op のメモリから。したがって、シングル スレッド バージョンに近い最終値、またはlockループ内でインクリメントを -ed した場合に得られる値になります。

特定のタイプの変数をアトミックに処理するためのハードウェア レベルの方法については、 Interlockedクラスを確認してください。

于 2011-08-20T13:37:28.603 に答える
1

共有変数 ms_Sum をロックしていない場合、両方のスレッドが ms_Sum 変数にアクセスし、値を無制限にインクリメントできます。デュアル コア マシンで並列に実行されている 2 つのスレッドは、変数に対して同時に動作します。

Memory: ms_Sum = 5
Thread1: ms_Sum += 1: ms_Sum = 5+1 = 6
Thread2: ms_Sum += 1: ms_Sum = 5+1 = 6 (running in parallel).

これは、私が説明できる最善の方法で起こっている大まかな内訳です​​。

1: ms_sum = 5.
2: (Thread 1) ms_Sum += 1;
3: (Thread 2) ms_Sum += 1;
4: (Thread 1) "read value of ms_Sum" -> 5
5: (Thread 2) "read value of ms_Sum" -> 5
6: (Thread 1) ms_Sum = 5+1 = 6
6: (Thread 2) ms_Sum = 5+1 = 6

同期/ロックを使用しない場合、予想される合計の約半分の結果が得られることは理にかなっています。これは、2 つのスレッドが "ほぼ" 2 倍の速度で処理できるためです。

適切な同期、つまりlock(ms_Lock) { ms_Counter += 1; }、順序は次のように変更されます。

 1: ms_sum = 5.
 2: (Thread 1) OBTAIN LOCK. ms_Sum += 1;
 3: (Thread 2) WAIT FOR LOCK.
 4: (Thread 1) "read value of ms_Sum" -> 5
 5: (Thread 1) ms_Sum = 5+1 = 6
 6. (Thread 1) RELEASE LOCK.
 7. (Thread 2) OBTAIN LOCK.  ms_Sum += 1;
 8: (Thread 2) "read value of ms_Sum" -> 6
 9: (Thread 2) ms_Sum = 6+1 = 7
10. (Thread 2) RELEASE LOCK.

「ほぼ」正しい理由についてlock(ms_Lock) {}; ms_Counter += 1;は、運が良かっただけだと思います。ロックは、各スレッドを強制的に減速させ、ロックを取得して解放するために「順番を待つ」ようにします。算術演算ms_Sum += 1;が非常に単純である (非常に高速に実行される) という事実が、おそらく結果が「ほぼ」OK である理由です。スレッド 2 がロックの取得と解放のオーバーヘッドを実行するまでに、単純な算術演算はスレッド 1 によって既に実行されている可能性が高いため、目的の結果に近づくことができます。より複雑なこと (より多くの処理時間がかかる) を行っていた場合、目的の結果に近づくことができないことがわかります。

于 2011-08-20T13:38:19.497 に答える
1

これが答えです。

他のすべての回答は長すぎて、正しくないものを見たので最後まで読みませんでした。回答はそれほど長くする必要はありません。おそらく、Sedat の回答が最も近いものでした。プログラムの速度を「遅くする」ロックステートメントとは実際には何の関係もありません。

これは、2 つのスレッド間の ms_sum のキャッシュ同期に関係しています。各スレッドには、ms_sum の独自のキャッシュ コピーがあります。

最初の例では、「ロック」を使用していないため、いつ同期を行うか (更新されたキャッシュ値をいつメイン メモリにコピーするか、いつメイン メモリから読み取るか) を OS に任せています。キャッシュ)。したがって、各スレッドは基本的に ms_sum の独自のコピーを更新しています。現在、同期はときどき発生しますが、すべてのスレッド コンテキスト スイッチでは発生しないため、結果は 50,000,000 を少し上回ります。すべてのスレッド コンテキスト スイッチで発生すると、10,000,000 になります。

2 番目の例では、ms_sum は反復ごとに同期されます。これにより、ms_sum #1 と ms_sum #2 が十分に同期されます。だから、あなたはほぼ10,000,000を得るつもりです。ただし、スレッド コンテキストが切り替わるたびに、ロックの外側で += が発生しているため、ms_sum が 1 ずつオフになる可能性があるため、10,000,000 まではいきません。

一般に、ロックが呼び出されたときにさまざまなスレッドのキャッシュのどの部分が同期されるかは、私には少しわかりません。しかし、2 番目の例でほぼ 10,000,000 という結果が得られたため、ロック呼び出しによって ms_sum が同期されていることがわかります。

于 2016-08-30T00:15:22.973 に答える