クラスが複数のスレッドによってアクセスされる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で実行されている複数のスレッドがデータをキャッシュし、命令を並べ替えることができるし、またそうすることです。
それがnotvolatile
で、CPU Aが値を増加させると、しばらくするとCPU Bは実際にはその増加された値を認識できず、これが問題を引き起こす可能性があります。
それがvolatile
であるなら、これはちょうど2つのCPUが同時に同じデータを見ることを保証します。それは彼らがあなたが避けようとしている問題である彼らの読み書き操作をインターリーブすることから彼らを全く止めません。
lock(this.locker) this.counter++
;
これは安全です(あなたがthis.counter
にアクセスする他の場所でlock
を忘れないでください)。他のスレッドがlocker
で保護されている他のコードを実行するのを防ぎます。ロックを使用すると、上記のようにマルチCPUの並べ替えの問題を防ぐことができます。これは素晴らしいことです。
問題は、ロックが遅いこと、そして実際には関係のない他の場所でlocker
を再利用すると、理由なく他のスレッドをブロックしてしまうことです。
Interlocked.Increment(ref this.counter);
これは安全です。中断することができない「1回のヒット」で読み取り、増分、および書き込みを効果的に実行できるためです。このため、他のコードには影響しません。また、他の場所でロックすることを忘れないでください。それはまた非常に速いです(MSDNが言うように、現代のCPUでは、これは文字通り単一のCPU命令です)。
しかし、それが他のCPUを回避して物事の順序を変えたのか、それとも揮発性と増分を組み合わせる必要があるのか、私は完全にはわかりません。
連動注:
volatile
はこのようなマルチスレッドの問題を防げないので、それは何のためのものですか?良い例は、2つのスレッドがあるということです。1つは常に変数に書き込み(queueLength
と言う)、もう1つは常に同じ変数から読み取ります。
queueLength
が揮発性ではない場合、スレッドAは5回書き込みますが、スレッドBはそれらの書き込みが遅延している(または場合によっては順序が間違っている)ことがあります。
解決策はロックすることですが、この状況でvolatileを使用することもできます。これにより、スレッドAが書き込んだ最新のものがスレッドBに常に表示されるようになります。ただし、この論理のみを読んだことのない作家と書いたことのない読者がいる場合はとあなたが書いているものがアトミック値であれば。単一の読み取り - 変更 - 書き込みを実行したらすぐに、インターロック操作に進むか、またはLockを使用する必要があります。
EDIT:コメントで述べたように、最近は単一変数の場合にInterlocked
を使うのが嬉しいです。明らかにはい。それがもっと複雑になっても、ロックに戻ります。
volatile
を使用しても、インクリメントする必要がある場合は役に立ちません - 読み取りと書き込みが別々の命令であるためです。読み終わってから書き戻す前に、別のスレッドが値を変更する可能性があります。
個人的には、私はほとんどいつもロックしています - ボラティリティやInterlocked.Incrementよりも明らかにrightのようにするのが簡単です。私の知る限りでは、ロックフリーのマルチスレッディングは本物のスレッディングのエキスパートのためのものですが、私はその一人ではありません。 Joe Duffyと彼のチームが私が構築するものほどロックすることなく物事を並列化するNiceライブラリを構築するならば、それは素晴らしいことです、そして私はそれをハートビートで使うつもりです - 複雑にしないでおく。
"volatile
"はInterlocked.Increment
に代わるものではありません。変数がキャッシュされずに直接使用されるようにするだけです。
変数を増やすには、実際には3つの操作が必要です。
Interlocked.Increment
は、3つの部分すべてを単一のアトミック操作として実行します。
あなたが探しているのは、ロックまたはインターロック増分のどちらかです。
現在のコードパスでコンパイラがメモリからの読み込みを最適化できる場合でも、Volatileは間違いなくあなたが望むものではありません - 単に変数を常に変化するものとして扱うようにコンパイラに指示します。
例えば.
while (m_Var)
{ }
別のスレッドでm_Varがfalseに設定されていてもvolatileとして宣言されていない場合、コンパイラはCPUレジスタに対してチェックすることで、無限ループにすることができます(ただし、常に実行されるわけではありません)。 m_Varのメモリ位置に別のreadを発行する代わりに、m_Varが最初からフェッチされたものです(これはキャッシュされる可能性があります - わからないし、気にする必要はありません。これがx86/x64のキャッシュ一貫性のポイントです)。命令の並べ替えについて述べた他の人による以前の記事はすべて、単にx86/x64アーキテクチャーを理解していないことを示しています。以前の投稿で「並べ替えを妨げる」と言っていたように、volatileは読み書きの障壁を発行しませんしません。実際、MESIプロトコルのおかげで、実際の結果が物理メモリにリタイアされたか、単にローカルCPUのキャッシュにあるかにかかわらず、読み取った結果がCPU間で常に同じになることが保証されます。この詳細については詳しく説明しませんが、これがうまくいかない場合、Intel/AMDがプロセッサのリコールを発行する可能性があることを安心してください。これはまた、順不同の実行などを気にする必要がないことを意味します。結果は常に順番に引退することが保証されています。
他のプロセッサが変更できないようにするには、キャッシュライン全体の排他的所有権(lock xadd)を保持しながら、プロセッサは指定されたアドレスから値を取得し、それから値を書き込みます。その価値.
Volatileを使っても、たった1命令で済むことになります(JITが効率的であると仮定した場合) - inc dword ptr [m_Var]。しかし、プロセッサ(cpuA)は、インターロックバージョンで行ったことのすべてを実行している間、キャッシュラインの排他的所有権を要求しません。ご想像のとおり、これは他のプロセッサがcpuAによって読み取られた後にm_Varに更新された値を書き戻すことを意味します。そのため、値を2倍に増やすのではなく、1回だけになります。
これが問題を解決することを願っています。
詳細については、「マルチスレッドアプリケーションでの低ロック技術の影響について」を参照してください - http://msdn.Microsoft.com/ja-jp/magazine/cc163715.aspx
pSこの非常に遅い回答を促したのは何ですか?すべての返事は(特に返事としてマークされているもの)彼らの説明で非常に露骨に間違っていました私はちょうどこれを読んでいる誰かのためにそれを片付けなければなりませんでした。 肩をすくめる
p.p.s.ターゲットはx86/x64でIA64ではないと想定しています(メモリモデルが異なります)。 MicrosoftのECMA仕様は、最強のメモリモデルではなく最弱のメモリモデルを指定するという点で問題があることに注意してください(プラットフォーム間で一貫性があるように常に最強のメモリモデルに対して指定することをお勧めします。 IntelはIA64用に同じように強力なメモリモデルを実装していますが、x64はIA64上でまったく動作しない可能性があります - Microsoftはこれを認めています - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17 /51445.aspx 。
連動機能はロックしません。これはアトミックです。つまり、インクリメント中にコンテキストが切り替わる可能性がなくても完了できるということです。そのため、デッドロックや待機の可能性はありません。
私はあなたがいつもそれをロックして増加することを好むべきであると言うでしょう。
あるスレッドで書き込みをして別のスレッドで読み込む必要がある場合や、オプティマイザが変数の操作を並べ替えないようにする場合(オプティマイザが認識していない別のスレッドで問題が発生しているため)は、volatileが便利です。それはあなたがどのように増加するかに対する直交的な選択です。
あなたがロックフリーコードについてもっと知りたいなら、これは本当に良い記事です、そして、それを書くことへのアプローチへの正しい方法
lock(...)は機能しますが、スレッドをブロックする可能性があり、他のコードが同じロックを互換性のない方法で使用していると、デッドロックを引き起こす可能性があります。
これを行うにはInterlocked。*が正しい方法です...最近のCPUはこれをプリミティブとしてサポートしているため、オーバーヘッドがはるかに少なくなります。
揮発性はそれ自体では正しくありません。変更された値を検索して書き込もうとしたスレッドは、同じことを行っている別のスレッドと競合する可能性があります。
私は2度目のJon Skeetの答えで、 "volatile"とInterlockedについてもっと知りたい人のために以下のリンクを追加したいと思います。
アトミック性、ボラティリティ、不変性は異なる、パート1 - (Eric Lippertのコーディングにおけるすばらしい冒険)
Sayonara Volatile - (2012年のJoe DuffyのウェブログのWayback Machineスナップショット)
理論が実際にどのように機能するかを確認するためにいくつかテストを行いました。 kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html 。私のテストはCompareExchnageに焦点を当てていましたが、Incrementの結果は似ています。マルチCPU環境では、インターロックは高速である必要はありません。これは、2歳の16 CPUサーバーにおけるIncrementのテスト結果です。テストには増加後の安全な読み取りも含まれますが、これは現実の世界では一般的なことです。
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
を含む他のタイプは、これらのタイプのフィールドへの読み取りと書き込みがアトミックであることを保証できないため、「volatile」とマークできません。これらのタイプのフィールドへのマルチスレッドアクセスを保護するには、Interlocked
クラスメンバを使用するか、lock
ステートメントを使用してアクセスを保護します。