揮発性vs.インターロックvs.ロック


670

クラスに、public int counter複数のスレッドによってアクセスされるフィールドがあるとしましょう。これintは、増分または減分されるだけです。

このフィールドをインクリメントするには、どのアプローチを使用する必要がありますか。その理由は何ですか。

  • lock(this.locker) this.counter++;
  • Interlocked.Increment(ref this.counter);
  • のアクセス修飾子をcounterに変更しますpublic volatile

私が発見したvolatileので、私は多くのlockステートメントとの使用を削除してきましたInterlocked。しかし、これを行わない理由はありますか?


C#リファレンスのスレッディングをお読みください。それはあなたの質問のインとアウトをカバーします。3つはそれぞれ目的と副作用が異なります。
スポールソン2008

1
simple-talk.com/blogs/2012/01/24/… 配列でのvolitableの使用を見ることができますが、完全には理解していませんが、これが何をするかについての別のリファレンスです。
eran otzap 2013

50
これは、「スプリンクラーシステムがアクティブにならないことを発見したので、それを削除して煙警報器と交換する」と言っているようなものです。これを行わない理由は、それが信じられないほど危険であり、ほとんど利益をもたらさないためです。コードの変更に費やす時間があれば、マルチスレッドを減らす方法を見つけてください!マルチスレッド化されたコードをより危険で簡単に破壊する方法を見つけないでください!
Eric Lippert、2014

1
私の家にはスプリンクラー煙探知器の両方があります。あるスレッドでカウンターをインクリメントして別のスレッドで読み取る場合、ロック(またはインターロック)揮発性キーワードの両方が必要なようです。真実?
ヨーヨー2014年

2
@yoyoいいえ、両方は必要ありません。
David Schwartz

回答:


857

最悪(実際には機能しない)

のアクセス修飾子counterpublic 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);

これは、中断できない「1ヒット」で読み取り、増分、書き込みを効果的に行うため、安全です。このため、他のコードには影響せず、他の場所でロックすることを覚えておく必要もありません。また、非常に高速です(MSDNによると、最近のCPUでは、これは文字通り単一のCPU命令であることがよくあります)。

ただし、他のCPUが順序を変更する問題を回避できるのか、あるいはvolatileとインクリメントを組み合わせる必要があるのか​​は、完全にはわかりません。

インターロックされたメモ:

  1. インターロックされたメソッドは、任意の数のコアまたはCPUで同時に安全です。
  2. インターロックされたメソッドは、実行する命令の周囲に完全なフェンスを適用するため、並べ替えは発生しません。
  3. インターロックされたメソッドは、揮発性フィールドへのアクセスを必要とせず、サポートもしません

脚注:揮発性が実際に何に適しているか。

などvolatileのためにそれを何、マルチスレッドの問題のこれらの種類を防ぐことはできませんか?良い例は、2つのスレッドがあることです。1つは常に変数に書き込み(たとえばqueueLength)、もう1つは常に同じ変数から読み取ります。

queueLengthが揮発性でない場合、スレッドAは5回書き込みを行う可能性がありますが、スレッドBはそれらの書き込みが遅延している(または誤った順序である可能性さえある)と見なす場合があります。

解決策はロックすることですが、この状況ではvolatileを使用することもできます。これにより、スレッドAが書き込んだ最新のものがスレッドBに常に表示されるようになります。ただし、このロジック機能するのは、読み取りを行わないライターと書き込みを行わないリーダーがあり、作成ているものがアトミック値である場合のみです。単一の読み取り-変更-書き込みを行うとすぐに、インターロックされた操作に移動するか、ロックを使用する必要があります。


29
「もしあなたがvolatileとincrementを組み合わせる必要があるかどうかは完全にはわかりません。」参照によってvolatileを渡すことができないため、これらをAFAIKと組み合わせることはできません。ところで素晴らしい答え。
Hosam Aly

41
どうもありがとう!「どの揮発性が実際に良いか」に関するあなたの脚注は私が探していたものであり、揮発性をどのように使用したいかを確認しました。
ジャックボッシュ

7
言い換えると、varがvolatileとして宣言されている場合、コンパイラーは、コードがそれに遭遇するたびに、varの値が同じ(つまり、volatile)のままではないと想定します。したがって、次のようなループでは、while(m_Var){}、およびm_Varが別のスレッドでfalseに設定されている場合、コンパイラーは、以前にm_Varの値がロードされたレジスターの内容を単にチェックするのではなく、m_Varから値を読み取ります再び。ただし、揮発性を宣言しないとループが無限に続くことを意味するわけではありません。揮発性を指定すると、m_Varが別のスレッドでfalseに設定されている場合にのみ保証されます。
Zach Saw

35
@Zach Saw:C ++のメモリモデルでは、volatileはそれをどのように説明したかを示します(基本的にはデバイスマップメモリ​​に役立ち、それ以外には多くありません)。CLRのメモリモデル(この質問にはC#のタグが付けられています)の下では、volatileは、その記憶場所への読み取りと書き込みの周りにメモリバリアを挿入します。メモリバリア(およびいくつかのアセンブリ命令の特別なロックされたバリエーション)は、プロセッサに順序を変更しないように指示し、それらはかなり重要です...
Orion Edwards

19
@ZachSaw:C#の揮発性フィールドは、C#コンパイラとjitコンパイラが値をキャッシュする特定の最適化を行うのを防ぎます。また、複数のスレッドでの読み取りと書き込みの順序を確認することもできます。実装の詳細として、読み取りと書き込みにメモリバリアを導入することで実現できます。保証されている正確なセマンティクスは仕様に記載されています。仕様では、すべての揮発性書き込みと読み取りの一貫した順序がすべてのスレッドで監視されることを保証していないことに注意してください。
Eric Lippert、

147

EDIT:コメントで述べたように、これらの日、私が使用して満足しているInterlockedの例のための単一の変数、それはです明らかに大丈夫。さらに複雑になっても、ロックに戻ります...

volatileインクリメントする必要がある場合は、使用しても効果がありません。読み取りと書き込みは別々の命令であるためです。別のスレッドは、あなたが読んだ後、あなたが書き戻す前に値を変更することができます。

個人的に私はほとんど常にロックするだけです- ボラティリティやインターロックよりも明らかに正しい方法で正しい方が簡単です。私に関する限り、ロックフリーのマルチスレッド化は、実際のスレッド化の専門家のためのものです。Joe Duffyと彼のチームが、自分が構築したものと同じくらいロックせずに物事を並列化する素晴らしいライブラリを構築する場合、それは素晴らしいことです。そして、それをハートビートで使用します。複雑にしないでおく。


16
これからロックフリーのコーディングを忘れるようにするための+1。
Xaqron

5
ロックフリーコードは、ある段階でロックされるため、完全にロックフリーではありません。(FSB)バスレベルでも、CPU間レベルでも、支払う必要があるペナルティがまだあります。ただし、これらの低いレベルでのロックは、ロックが発生する場所の帯域幅を飽和させない限り、一般的に高速です。
Zach Saw、

2
Interlockedには何の問題もありません。それはまさにあなたが探しているものであり、完全なlock()よりも高速です
Jaap

5
@Jaap:はい、最近で、本物の単一のカウンターにインターロックを使用します。変数に対する複数のロックなしの更新間の相互作用を解決しようとしていじり始めたくないだけです。
Jon Skeet、2012年

6
@ZachSaw:2番目のコメントは、インターロックされた操作がある段階で "ロック"することを示しています。「ロック」という用語は一般に、1つのタスクが無制限の時間リソースの排他制御を維持できることを意味します。ロックフリープログラミングの主な利点は、所有するタスクがわずらわしくなり、リソースが使用できなくなる危険性を回避できることです。インターロックされたクラスで使用されるバス同期は、「一般的に高速」ではありません。ほとんどのシステムでは、最悪の場合の時間に制限がありますが、ロックにはありません。
スーパーキャット2012

44

volatile」は置き換えられませんInterlocked.Increment。変数がキャッシュされず、直接使用されることを確認するだけです。

変数をインクリメントするには、実際には3つの操作が必要です。

  1. 読んだ
  2. インクリメント
  3. 書く

Interlocked.Increment 3つの部分すべてを1つのアトミック操作として実行します。


4
別の言い方をすると、インターロックされた変更は完全にフェンスで囲まれているため、アトミックです。揮発性メンバーは部分的にフェンスされているだけなので、スレッドセーフであるとは保証されていません。
JoeGeeky

1
実際にvolatileは、変数がキャッシュされていないことを確認しませ。キャッシュする方法に制限を設けるだけです。たとえば、ハードウェアで一貫性が保たれているため、CPUのL2キャッシュにキャッシュすることができます。それはまだ完成することができます。書き込みは引き続きキャッシュにポストできます。(ザックがやっていることだったと思います。)
デビッドシュワルツ

42

ロックまたはインターロックされた増分のいずれかが、あなたが探しているものです。

揮発性は間違いなくあなたが求めているものではありません-現在のコードパスでコンパイラがメモリからの読み取りを最適化できない場合でも、変数を常に変更するものとして扱うようコンパイラーに指示するだけです。

例えば

while (m_Var)
{ }

別のスレッドでm_Varがfalseに設定されているが、それがvolatileとして宣言されていない場合、コンパイラーは、CPUレジスター(たとえば、 m_Varのメモリの場所に別の読み取りを発行する代わりに、m_Varが最初からフェッチされたもの(これはキャッシュされる可能性があります-不明であり、気にしていません。これがx86 / x64のキャッシュ一貫性のポイントです)。命令の並べ替えについて言及した他の人による以前のすべての投稿は、単にx86 / x64アーキテクチャを理解していないことを示しています。揮発性ではありませ以前の投稿で暗示されているように、「並べ替えを妨げる」と読み書きバリアを発行します。実際、MESIプロトコルのおかげで、実際の結果が物理メモリにリタイアされたか、単にローカルCPUのキャッシュに常駐しているかに関係なく、読み取った結果がCPU全体で常に同じであることが保証されます。詳細についてはあまり触れませんが、これがうまくいかない場合は、Intel / AMDがプロセッサのリコールを発行する可能性があるのでご安心ください。これはまた、順不同の実行などを気にする必要がないことも意味します。結果は常に順番にリタイアすることが保証されます-そうでなければ、詰め込まれます!

Interlocked Incrementを使用すると、プロセッサは出て行き、指定されたアドレスから値をフェッチしてからインクリメントして書き戻す必要があります。その間、キャッシュライン全体(lock xadd)の排他的所有権を持ち、他のプロセッサが変更できないようにします。その価値。

volatileを使用しても、1つの命令しか得られません(JITが効率的であると想定)-inc dword ptr [m_Var]。ただし、プロセッサ(cpuA)は、インターロックされたバージョンで行ったすべてを実行している間、キャッシュラインの排他的な所有権を要求しません。ご想像のとおり、これは、cpuAによって読み取られた後、他のプロセッサーが更新された値をm_Varに書き戻すことができることを意味します。したがって、値を2回インクリメントする代わりに、結果は1回だけになります。

これで問題が解決することを願っています。

詳細については、「マルチスレッドアプリでのローロックテクニックの影響について」-http ://msdn.microsoft.com/en-au/magazine/cc163715.aspxを参照してください。

psこの非常に遅い返信のきっかけは何ですか?すべての返信は非常に露骨に不正確であり(特に回答としてマークされたもの)、説明を他の人が読んでいる人のためにそれを明確にする必要がありました。肩をすくめる

ppsターゲットはIA64ではなくx86 / x64であると想定しています(メモリモデルが異なります)。MicrosoftのECMA仕様は、最も強いメモリモデルではなく最も弱いメモリモデルを指定するという点で台無しになっていることに注意してください(プラットフォーム間で一貫性があるように、最も強いメモリモデルに対して指定することを常にお勧めします。それ以外の場合、x86 /で24〜7で実行されるコードIntelが同様に強力なメモリモデルをIA64に実装している場合でも、x64はIA64でまったく実行されない可能性があります)-Microsoft自身もこれを認めています-http ://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx


3
面白い。これを参照できますか?私は喜んでこれに投票しますが、私が読んだリソースと一致する非常に投票された回答から3年後に積極的な言葉で投稿するには、もう少し具体的な証拠が必要になります。
Steven Evers

参照したい部分を指すことができる場合は、どこかから何かを掘り下げて喜んでいます(x86 / x64ベンダーの企業秘密を教えてくれなかったので、これらはwiki、Intelから簡単に入手できるはずです) PRM(プログラマー向けリファレンスマニュアル)、MSFTブログ、MSDNまたは類似のもの)...
Zach Saw

2
CPUがキャッシュされないようにしたいと思う理由は、私を超えています。キャッシュコヒーレンシを実行するために費やされたすべての不動産(サイズとコストは無視できません)は、完全に無駄になります...グラフィックカード、PCIデバイスなどのキャッシュコヒーレンシが必要でない限り、設定しません。ライトスルーするキャッシュライン。
Zach Saw、

4
はい、あなたが言うすべては100%ではないとしても、少なくとも99%はマークです。このサイトは、(ほとんどの場合)仕事で開発が急いでいるときにかなり役に立ちますが、残念ながら(ゲームの)投票に対応する回答の正確性はありません。したがって、基本的にスタックオーバーフローでは、読者が実際に何であるかではなく、読者の一般的な理解を理解することができます。時々、トップの答えは単なる意味不明の神話です。そして、残念ながら、これは問題を解決している間に本を読んだ人々に出会うものです。しかしそれは理解できますが、誰もがすべてを知ることはできません。
user1416420

1
@BenVoigt .NETが動作するすべてのアーキテクチャについて続行して回答することはできますが、これには数ページかかるため、SOには明らかに適していません。最も広く使用されている.NETの基礎となるハードウェアmemモデルに基づいて人々を教育することは、恣意的なものよりもはるかに優れています。そして、「どこでも」のコメントで、私は人々がキャッシュのフラッシュ/無効化などを仮定する際に犯した間違いを修正していました。彼らはどのハードウェアを指定せずに基礎となるハードウェアについての仮定を行いました。
Zach Saw

16

インターロックされた関数はロックされません。これらはアトミックです。つまり、インクリメント中にコンテキストが切り替わることなく完了することができます。したがって、デッドロックや待機の可能性はありません。

ロックとインクリメントよりも常にそれを好むべきだと私は言うでしょう。

あるスレッドでの書き込みを別のスレッドで読み取る必要があり、オプティマイザーが変数の操作を並べ替えないようにする場合(揮発性は、オプティマイザーが認識していない別のスレッドで発生しているため)に役立ちます。これは、増分方法とは正反対の選択です。

これは、ロックフリーのコードと、それを書くための適切な方法についてもっと知りたい場合、本当に良い記事です。

http://www.ddj.com/hpc-high-performance-computing/210604448


11

lock(...)は機能しますが、スレッドをブロックする可能性があり、他のコードが同じロックを互換性のない方法で使用している場合、デッドロックを引き起こす可能性があります。

Interlocked。*がそれを行う正しい方法です...最近のCPUがこれをプリミティブとしてサポートしているため、オーバーヘッドがはるかに少なくなります。

volatile自体は正しくありません。変更された値を取得して書き戻しようとするスレッドは、同じことをしている別のスレッドと競合する可能性があります。


8

理論が実際にどのように機能するかを確認するためにいくつかのテストを行いました:kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html。私のテストはCompareExchnageに重点を置いていましたが、インクリメントの結果も同様です。マルチ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

あなたがテストしたコードサンプルはすごく取るに足らないものでした-そのようにテストすることは実際にはあまり意味がありません!最善の方法は、さまざまな方法で実際に何が行われているかを理解し、使用シナリオに基づいて適切な方法を使用することです。
Zach Saw

@Zach、ここでの議論は、スレッドセーフな方法でカウンターを増やすシナリオについてでした。他にどのような使用シナリオが考えられましたか、またはどのようにテストしますか?BTWコメントありがとうございます。
Kenneth Xu

ポイントは、それは人工的なテストです。実際のシナリオでよく見られるのと同じ場所を叩くつもりはありません。もしそうなら、(サーバーボックスに示されているように)FSBによってボトルネックになっています。とにかく、あなたのブログで私の返事を見てください。
Zach Saw、

2
もう一度振り返ります。真のボトルネックがFSBにある場合、モニター実装は同じボトルネックを監視する必要があります。本当の違いは、Interlockedがビジーな待機と再試行を行っていることです。これは、高パフォーマンスのカウントでは実際の問題になります。少なくとも、私のコメントが、Interlockedが常に数えるのに正しい選択であるとは限らないという注意を喚起することを望みます。人々が選択肢を探しているという事実はそれをよく説明しました。長い加算器が必要ですgee.cs.oswego.edu/dl/jsr166/dist/jsr166edocs/jsr166e/…–
Kenneth Xu

8

3

私は他の回答の違いに言及したに追加したいvolatileInterlockedlock

volatileキーワードは、次のタイプのフィールドに適用できます

  • 参照タイプ。
  • ポインタ型(安全でないコンテキスト)。ポインタ自体は揮発性である可能性がありますが、それが指すオブジェクトは揮発性ではないことに注意してください。つまり、「ポインタ」を「揮発性」であると宣言することはできません。
  • 以下のような単純型sbytebyteshortushortintuintcharfloat、とbool
  • 下記塩基のいずれかのタイプと列挙型:bytesbyteshort、USHORT、intまたはuint
  • 参照型として知られているジェネリック型パラメーター。
  • IntPtrUIntPtr

doubleおよびを含む他のタイプはlongそれらのタイプのフィールドへの読み取りおよび書き込みがアトミックであることが保証されていないため、「揮発性」としてマークできません。これらのタイプのフィールドへのマルチスレッドアクセスを保護するには、Interlockedクラスメンバーを使用するか、lockステートメントを使用してアクセスを保護します 。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.