C# の volatile キーワードの説明を誰かが提供できますか? どの問題が解決され、どの問題が解決されないか? ロックを使用しなくても済むのはどのような場合ですか?
11 に答える
Eric Lippertよりもこれに答えるのに適した人はいないと思います(原文で強調):
C# では、「揮発性」とは、「コンパイラとジッタがコードの並べ替えを実行したり、この変数でキャッシュの最適化を登録したりしないことを確認する」ことを意味するだけではありません。また、「他のプロセッサを停止してメインメモリをキャッシュと同期させることを意味する場合でも、最新の値を読み取っていることを確認するために必要なことは何でも行うようにプロセッサに指示する」ことも意味します。
実際、最後のビットは嘘です。揮発性の読み取りと書き込みの真のセマンティクスは、ここで概説したよりもかなり複雑です。実際、すべてのプロセッサが実行中の処理を停止し、メイン メモリとの間でキャッシュを更新することを実際に保証するわけではありません。むしろ、それらは、読み取りと書き込みの前後のメモリアクセスが相互にどのように順序付けられていることが観察されるかについて、より弱い保証を提供します。新しいスレッドの作成、ロックの開始、Interlocked ファミリーのメソッドの使用などの特定の操作では、順序付けの監視に関してより強力な保証が導入されます。詳細については、C# 4.0 仕様のセクション 3.10 および 10.5.3 を参照してください。
率直に言って、不安定なフィールドを作成しないようお勧めします。揮発性フィールドは、まったくおかしなことをしている兆候です。ロックを設定せずに、2 つの異なるスレッドで同じ値を読み書きしようとしています。ロックは、ロック内で読み取りまたは変更されたメモリが一貫していることを保証し、ロックは一度に 1 つのスレッドのみが特定のメモリ チャンクにアクセスすることを保証します。ロックが遅すぎる状況の数は非常に少なく、正確なメモリ モデルを理解していないためにコードを間違える可能性は非常に高くなります。インターロック操作の最も些細な使用法を除いて、ローロック コードを記述しようとはしません。「揮発性」の使用法は、本当の専門家に任せます。
詳細については、以下を参照してください。
volatile キーワードの機能についてもう少し技術的に知りたい場合は、次のプログラムを検討してください (私は DevStudio 2005 を使用しています)。
#include <iostream>
void main()
{
int j = 0;
for (int i = 0 ; i < 100 ; ++i)
{
j += i;
}
for (volatile int i = 0 ; i < 100 ; ++i)
{
j += i;
}
std::cout << j;
}
標準の最適化 (リリース) コンパイラ設定を使用して、コンパイラは次のアセンブラ (IA32) を作成します。
void main()
{
00401000 push ecx
int j = 0;
00401001 xor ecx,ecx
for (int i = 0 ; i < 100 ; ++i)
00401003 xor eax,eax
00401005 mov edx,1
0040100A lea ebx,[ebx]
{
j += i;
00401010 add ecx,eax
00401012 add eax,edx
00401014 cmp eax,64h
00401017 jl main+10h (401010h)
}
for (volatile int i = 0 ; i < 100 ; ++i)
00401019 mov dword ptr [esp],0
00401020 mov eax,dword ptr [esp]
00401023 cmp eax,64h
00401026 jge main+3Eh (40103Eh)
00401028 jmp main+30h (401030h)
0040102A lea ebx,[ebx]
{
j += i;
00401030 add ecx,dword ptr [esp]
00401033 add dword ptr [esp],edx
00401036 mov eax,dword ptr [esp]
00401039 cmp eax,64h
0040103C jl main+30h (401030h)
}
std::cout << j;
0040103E push ecx
0040103F mov ecx,dword ptr [__imp_std::cout (40203Ch)]
00401045 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (402038h)]
}
0040104B xor eax,eax
0040104D pop ecx
0040104E ret
出力を見ると、コンパイラは ecx レジスタを使用して j 変数の値を格納することを決定しています。不揮発性ループ (最初のループ) の場合、コンパイラは i を eax レジスタに割り当てています。かなり簡単です。ただし、興味深いビットがいくつかあります。lea ebx,[ebx] 命令は実質的にマルチバイトの nop 命令であるため、ループは 16 バイトでアラインされたメモリ アドレスにジャンプします。もう 1 つは、inc eax 命令を使用する代わりに、edx を使用してループ カウンターをインクリメントする方法です。add reg,reg 命令は、いくつかの IA32 コアで inc reg 命令と比較してレイテンシが低くなりますが、レイテンシが高くなることはありません。
次に、揮発性ループ カウンターを使用したループについて説明します。カウンターは [esp] に格納され、volatile キーワードは、値を常にメモリから読み書きする必要があり、レジスタに割り当ててはならないことをコンパイラに伝えます。コンパイラは、カウンタ値を更新するときに、ロード/インクリメント/ストアを 3 つの異なるステップ (load eax、inc eax、save eax) として実行するのではなく、単一の命令 (add mem) でメモリを直接変更します。 、登録)。コードが作成された方法により、単一の CPU コアのコンテキスト内でループ カウンターの値が常に最新であることが保証されます。データを操作しないと、破損やデータ損失が発生する可能性があります (したがって、値は inc 中に変更される可能性があり、ストアで失われる可能性があるため、load/inc/store を使用しないでください)。割り込みは、現在の命令が完了した後にのみ処理できるため、
システムに 2 番目の CPU を導入すると、volatile キーワードは、別の CPU によって同時に更新されるデータを保護しません。上記の例では、データが破損する可能性があるため、データを非整列にする必要があります。volatile キーワードは、データをアトミックに処理できない場合、潜在的な破損を防ぐことはできません。たとえば、ループ カウンターのタイプが long long (64 ビット) の場合、値を更新するために 2 つの 32 ビット操作が必要になります。割り込みが発生してデータが変更される可能性があります。
そのため、volatile キーワードは、操作が常にアトミックであるように、ネイティブ レジスタのサイズ以下のアラインされたデータにのみ適しています。
volatile キーワードは、IO が絶えず変化するが、メモリ マップされた UART デバイスなどの一定のアドレスを持つ IO 操作で使用することを想定しており、コンパイラはアドレスから読み取った最初の値を再利用し続けるべきではありません。
大規模なデータを処理している場合、または複数の CPU を使用している場合は、データ アクセスを適切に処理するために、より高いレベル (OS) のロック システムが必要になります。
.NET 1.1 を使用している場合は、ダブル チェック ロックを行うときに volatile キーワードが必要です。なんで?.NET 2.0 より前のバージョンでは、次のシナリオにより、2 番目のスレッドが null ではないものの、完全には構築されていないオブジェクトにアクセスする可能性がありました。
- スレッド 1 は、変数が null かどうかを尋ねます。//if(this.foo == null)
- スレッド 1 は、変数が null であると判断するため、ロックに入ります。//lock(this.bar)
- スレッド 1 は、変数が null かどうかを AGAIN に問い合わせます。//if(this.foo == null)
- スレッド 1 は引き続き変数が null であると判断するため、コンストラクターを呼び出して値を変数に割り当てます。//this.foo = new Foo();
.NET 2.0 より前では、コンストラクターの実行が完了する前に、this.foo に Foo の新しいインスタンスを割り当てることができました。この場合、2 番目のスレッドが (スレッド 1 の Foo のコンストラクターへの呼び出し中に) 入り、次のことが発生する可能性があります。
- スレッド 2 は、変数が null かどうかを尋ねます。//if(this.foo == null)
- スレッド 2 は、変数が null ではないと判断したため、それを使用しようとします。//this.foo.MakeFoo()
.NET 2.0 より前では、この問題を回避するために this.foo を volatile として宣言できました。.NET 2.0 以降、二重チェック ロックを実現するために volatile キーワードを使用する必要がなくなりました。
ウィキペディアにはダブル チェック ロックに関する優れた記事があり、このトピックについて簡単に触れています: http://en.wikipedia.org/wiki/Double-checked_locking
場合によっては、コンパイラーはフィールドを最適化し、レジスターを使用してそれを保管します。スレッド1がフィールドに書き込みを行い、別のスレッドがフィールドにアクセスする場合、更新はレジスタ(メモリではなく)に格納されているため、2番目のスレッドは古いデータを取得します。
volatileキーワードは、コンパイラーに「この値をメモリーに保管してほしい」と言っていると考えることができます。これにより、2番目のスレッドが最新の値を取得することが保証されます。
MSDNから: 通常、volatile 修飾子は、アクセスをシリアル化するために lock ステートメントを使用せずに、複数のスレッドによってアクセスされるフィールドに使用されます。volatile 修飾子を使用すると、あるスレッドが別のスレッドによって書き込まれた最新の値を確実に取得できます。
CLRは命令を最適化するのが好きなので、コード内のフィールドにアクセスするときに、フィールドの現在の値に常にアクセスするとは限りません(スタックなどからのものである可能性があります)。フィールドにとしてマークを付けると、フィールドvolatile
の現在の値に命令がアクセスできるようになります。これは、プログラム内の並行スレッドまたはオペレーティングシステムで実行されている他のコードによって値を(非ロックシナリオで)変更できる場合に役立ちます。
明らかに最適化が失われますが、コードはより単純になります。
volatile キーワードの公式ページを調べるだけで、典型的な使用例を見ることができます。
public class Worker
{
public void DoWork()
{
bool work = false;
while (!_shouldStop)
{
work = !work; // simulate some work
}
Console.WriteLine("Worker thread: terminating gracefully.");
}
public void RequestStop()
{
_shouldStop = true;
}
private volatile bool _shouldStop;
}
_shouldStop の宣言に volatile 修飾子を追加すると、常に同じ結果が得られます。ただし、_shouldStop メンバーにその修飾子がないと、動作は予測できません。
したがって、これはまったくおかしなことではありません。
CPU キャッシュの一貫性を担うキャッシュ コヒーレンスが存在します。
また、CPU が強力なメモリ モデルを採用している場合(x86 など)
その結果、x86 では揮発性フィールドの読み取りと書き込みに特別な命令は必要ありません。通常の読み取りと書き込み (たとえば、MOV 命令を使用) で十分です。
C# 5.0 仕様の例 (第 10.5.3 章)
using System;
using System.Threading;
class Test
{
public static int result;
public static volatile bool finished;
static void Thread2() {
result = 143;
finished = true;
}
static void Main() {
finished = false;
new Thread(new ThreadStart(Thread2)).Start();
for (;;) {
if (finished) {
Console.WriteLine("result = {0}", result);
return;
}
}
}
}
出力を生成します: result = 143
フィールド finished が volatile であると宣言されていない場合、ストアが終了した後にストアの結果がメイン スレッドに表示されることは許容されます。したがって、メイン スレッドはフィールドの結果から値 0 を読み取ることができます。
揮発性の動作はプラットフォームに依存するためvolatile
、ニーズを確実に満たすために、必要に応じて使用することを常に検討する必要があります。
volatile
(あらゆる種類の) 並べ替えを防ぐことさえできませんでした ( C# - The C# Memory Model in Theory and Practice, Part 2 )
A への書き込みは揮発性であり、A_Won からの読み取りも揮発性ですが、フェンスは両方とも一方向であり、実際にこの並べ替えが可能です。
volatile
したがって、いつ(vs lock
vs )を使用するかを知りたい場合はInterlocked
、メモリ フェンス (フル、ハーフ) と同期の必要性に慣れる必要があると思います。そうすれば、あなたの貴重な答えを自分で得ることができます。