揮発性 vs.連動 vs.ロック
-
03-07-2019 - |
質問
クラスに public int counter
複数のスレッドによってアクセスされるフィールド。これ int
増加または減少するだけです。
このフィールドを増やすには、どのアプローチを使用する必要がありますか?またその理由は何ですか?
lock(this.locker) this.counter++;
,Interlocked.Increment(ref this.counter);
,- のアクセス修飾子を変更します
counter
にpublic volatile
.
今、発見しました volatile
, たくさん削除してきました。 lock
ステートメントとその使用 Interlocked
. 。しかし、そうしない理由はあるのでしょうか?
解決
最悪(実際には動作しません)
のアクセス修飾子を変更します
counter
にpublic volatile
他の人も述べているように、これ自体は実際にはまったく安全ではありません。のポイント volatile
それは、複数の CPU 上で実行されている複数のスレッドがデータをキャッシュし、命令を再順序付けできることです。
もしそれが ない volatile
, 、CPU A が値をインクリメントすると、CPU B はしばらくしてからそのインクリメントされた値を実際に確認できない可能性があり、問題が発生する可能性があります。
もしそれが volatile
, これは、2 つの CPU が同時に同じデータを参照できるようにするだけです。それは、あなたが避けようとしている問題である、読み取りと書き込みの操作をインターリーブすることをまったく妨げません。
セカンドベスト:
lock(this.locker) this.counter++
;
これは安全に実行できます (忘れない限り) lock
他のどこにアクセスしても this.counter
)。他のスレッドが、によって保護されている他のコードを実行するのを防ぎます。 locker
。ロックを使用すると、上記のようなマルチ CPU の並べ替えの問題も防ぐことができ、これは非常に優れています。
問題は、ロックが遅いことです。 locker
実際には関係のない別の場所で、理由もなく他のスレッドをブロックしてしまう可能性があります。
最高
Interlocked.Increment(ref this.counter);
これは、読み取り、増分、書き込みを中断できない「ワンヒット」で効果的に実行するため、安全です。このため、他のコードには影響せず、他の場所でロックすることを忘れる必要もありません。また、非常に高速です (MSDN によると、最新の CPU では、多くの場合、これは文字通り単一の CPU 命令です)。
ただし、他の CPU の順序変更を回避できるのか、それとも volatile と増分を組み合わせる必要があるのかはよくわかりません。
連動メモ:
- インターロック方式は、任意の数のコアまたは CPU 上で同時に安全に実行できます。
- インターロックされたメソッドは、実行する命令の周囲に完全なフェンスを適用するため、並べ替えは発生しません。
- 連動方式 揮発性フィールドへのアクセスは必要ないか、サポートすらしていません, 、 volatile は指定されたフィールドの操作の周囲にハーフ フェンスを配置し、interlocked はフル フェンスを使用します。
脚注:揮発性が実際に何に適しているのか。
として volatile
この種のマルチスレッドの問題は防止できませんが、何のためにあるのでしょうか?良い例は、2 つのスレッドがあり、1 つは常に変数に書き込むものであるとします (たとえば、 queueLength
) と、常に同じ変数から読み取るものです。
もし queueLength
が揮発性ではない場合、スレッド A は 5 回書き込みを行う可能性がありますが、スレッド B はそれらの書き込みが遅れている (または潜在的に間違った順序である) と見なす可能性があります。
解決策はロックすることですが、この状況では volatile を使用することもできます。これにより、スレッド B はスレッド A が書き込んだ最新のものを常に参照できるようになります。ただし、このロジックは のみ 全く読まない作家と全く書かない読者がいる場合には機能しますが、 そして 書いているものがアトミック値の場合。読み取り、変更、書き込みを 1 回実行したらすぐに、インターロック操作に進むか、ロックを使用する必要があります。
他のヒント
編集:コメントで述べたように、最近では単一変数の場合に Interlocked
を使用して< em>もちろん大丈夫です。複雑になった場合でも、ロックに戻ります...
volatile
を使用しても、増分が必要な場合は役に立ちません。これは、読み取りと書き込みが別々の命令であるためです。別のスレッドは、読み取り後、書き戻す前に値を変更できます。
個人的には、ほとんど常にロックするだけです。ボラティリティやインターロックのいずれかよりも、明らかに正しい方法で正しい方法を取得する方が簡単です。私の知る限り、ロックフリーのマルチスレッド化は、実際のスレッド化の専門家向けであり、私はそうではありません。 Joe Duffyと彼のチームが、私が構築するものと同じくらいロックせずに物事を並列化する素敵なライブラリを構築する場合、それは素晴らしいです、そして私はそれをハートビートで使用します-しかし、私は自分でスレッドをやっているとき、シンプルにしてください。
&quot; volatile
&quot; Interlocked.Increment
を置き換えません!変数がキャッシュされず、直接使用されることを確認するだけです。
変数をインクリメントするには、実際には3つの操作が必要です。
- 読む
- 増分
- 書き込み
Interlocked.Increment
は、3つの部分すべてを単一のアトミック操作として実行します。
どちらのロックまたはインターロックされた増分があなたが探しているものです。
Volatileは間違いなくあなたが望んでいるものではありません-現在のコードパスでコンパイラがメモリからの読み取りを最適化できる場合でも、変数を常に変化するものとして扱うようコンパイラに指示します。
e.g。
while (m_Var)
{ }
別のスレッドでm_Varがfalseに設定されているが、volatileとして宣言されていない場合、コンパイラーはCPUレジスター(EAXなど)をチェックすることにより、それを無限ループにすることができます(常にそうなるわけではありません)これは、m_Varのメモリ位置に別の読み取りを発行するのではなく、m_Varが最初からフェッチされたものであるためです(これはキャッシュされる場合があります。 x64)。命令の並べ替えに言及した他の人による以前のすべての投稿は、単にx86 / x64アーキテクチャを理解していないことを示しています。揮発性は、「並べ替えを防ぐ」という以前の投稿で暗示されているように、読み取り/書き込みバリアを発行しません。実際、再びMESIプロトコルのおかげで、実際の結果が物理メモリに廃棄されたか、単にローカルCPUのキャッシュにあるかに関係なく、読み取った結果はCPU全体で常に同じであることが保証されます。これについてはあまり詳しく説明しませんが、これがうまくいかない場合、Intel / AMDがプロセッサーのリコールを発行する可能性が高いので安心してください!これはまた、順不同の実行などを気にする必要がないことも意味します。結果は常に順序どおりに引退することが保証されます-そうでない場合は詰められます!
Interlocked Incrementでは、プロセッサは外に出て、指定されたアドレスから値をフェッチし、増分して書き戻す必要がありますが、キャッシュライン全体の排他的所有権を持ち(ロックxadd)、他のプロセッサはその値を変更できます。
volatileを使用しても、たった1つの命令で終了します(JITが効率的であると想定)-inc dword ptr [m_Var]。ただし、プロセッサ(cpuA)は、キャッシュバージョンの排他的所有権を要求しませんが、インターロックバージョンですべてを実行します。ご想像のとおり、これは他のプロセッサーがcpuAによって読み取られた後、更新された値をm_Varに書き戻すことができることを意味します。したがって、値を2回インクリメントする代わりに、1回だけで終了します。
これで問題が解決することを期待してください。
詳細については、「マルチスレッドアプリのローロックテクニックの影響を理解する」を参照してください- http://msdn.microsoft.com/en-au/magazine/cc163715.aspx
p.s。この非常に遅い返信のきっかけは何ですか?説明の中で、すべての返信は非常に露骨に間違っていました(特に、回答としてマークされたもの)。 shrugs
p.p.s。ターゲットはIA64ではなくx86 / x64であると想定しています(メモリモデルが異なります)。 MicrosoftのECMA仕様は、最強のメモリモデルではなく最弱のメモリモデルを指定するという点で台無しになっていることに注意してください(最強のメモリモデルに対して指定する方が、プラットフォーム間で一貫性があり、そうでない場合はx86 / IntelはIA64に同様の強力なメモリモデルを実装していますが、x64はIA64でまったく動作しない場合があります)-Microsoft自身もこれを認めています- http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx 。
インターロックされた関数はロックしません。これらはアトミックです。つまり、増分中にコンテキストを切り替えることなく完了することができます。そのため、デッドロックや待機の可能性はありません。
ロックとインクリメントよりも常に優先すべきだと思います。
Volatileは、あるスレッドでの書き込みを別のスレッドで読み取る必要がある場合、およびオプティマイザーが変数の操作を並べ替えないようにする場合に便利です(オプティマイザーが知らない別のスレッドで発生しているため)。増分方法に直交する選択です。
これは、ロックフリーコードについてさらに詳しく知りたい場合、およびそれを記述するための正しい方法について本当に良い記事です
http://www.ddj.com/hpc-high-performance- computing / 210604448
lock(...)は機能しますが、スレッドをブロックする可能性があり、他のコードが同じロックを互換性のない方法で使用している場合、デッドロックを引き起こす可能性があります。
Interlocked。*はそれを行う正しい方法です。現代のCPUはこれをプリミティブとしてサポートしているため、オーバーヘッドがはるかに少なくなります。
volatile自体は正しくありません。変更された値を取得して書き戻そうとするスレッドは、同じことを行う別のスレッドと競合する可能性があります。
理論が実際にどのように機能するかを確認するためにテストを行いました。 kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html 。私のテストはCompareExchnageに重点を置いていましたが、Incrementの結果は似ています。インターロックは、マルチCPU環境では高速である必要はありません。これは、2年前の16 CPUサーバーでの増分のテスト結果です。テストには、増加後の安全な読み取りも含まれることに留意してください。これは、現実の世界では一般的です。
D:\>InterlockVsMonitor.exe 16
Using 16 threads:
InterlockAtomic.RunIncrement (ns): 8355 Average, 8302 Minimal, 8409 Maxmial
MonitorVolatileAtomic.RunIncrement (ns): 7077 Average, 6843 Minimal, 7243 Maxmial
D:\>InterlockVsMonitor.exe 4
Using 4 threads:
InterlockAtomic.RunIncrement (ns): 4319 Average, 4319 Minimal, 4321 Maxmial
MonitorVolatileAtomic.RunIncrement (ns): 933 Average, 802 Minimal, 1018 Maxmial
volatile
、 Interlocked
、および lock
の違いを他の回答に記載されているものに追加したい:
volatileキーワードは、次のフィールドに適用できます。これらのタイプ:
- 参照タイプ。
- ポインタータイプ(安全でないコンテキスト内)。ポインター自体は揮発性である場合がありますが、ポインターが指すオブジェクトは揮発性ではないことに注意してください。他の 言葉、「ポインター」を宣言することはできません; 「揮発性」になります。
-
sbyte
、byte
、short
、ushort
、int
、uint
、char
、float
、およびbool
。 - 次の基本型のいずれかを持つ列挙型:
byte
、sbyte
、short
、ushort、int
、またはuint
。 - 参照型として知られている一般的な型パラメータ。
-
IntPtr
およびUIntPtr
。
double
や long
などの他の種類は、「揮発性」とマークできません。
これらのタイプのフィールドへの読み取りと書き込みは保証できないため
アトミックであること。これらのタイプへのマルチスレッドアクセスを保護するには
フィールド、 Interlocked
クラスメンバを使用するか、
lock
ステートメント。