誰でもC#の揮発性キーワードの良い説明を提供できますか?どの問題を解決し、どの問題を解決しませんか?どの場合にロックの使用を節約できますか?
Eric Lippert (オリジナルの強調)よりも、これに答える方が良いとは思いません。
C#では、「揮発性」とは、「コンパイラーとジッターがコードの並べ替えを行わないこと、またはこの変数のキャッシュ最適化を登録しないこと」を意味するだけではありません。また、「他のプロセッサを停止し、メインメモリとキャッシュを同期させることを意味する場合でも、最新の値を確実に読み取るために必要な処理をプロセッサに指示する」ことも意味します。
実際、その最後のビットは嘘です。揮発性の読み取りと書き込みの真のセマンティクスは、ここで説明したよりもかなり複雑です。実際、すべてのプロセッサが実行中の処理を停止することを保証するわけではなく、メインメモリとの間でキャッシュを更新します。むしろ、これらは、読み取りと書き込みの前後のメモリアクセスが互いに関して順序付けられていることが観察される方法についてより弱い保証を提供します。新しいスレッドの作成、ロックの入力、またはインターロックされたメソッドファミリの1つを使用するなどの特定の操作では、順序の監視についてより強力な保証が導入されます。さらに詳細が必要な場合は、C#4.0仕様のセクション3.10および10.5.3をお読みください。
率直に言って、揮発性フィールドを作成することはできません。揮発性フィールドは、まったくおかしなことをしていることを示しています。ロックを設定せずに、2つの異なるスレッドで同じ値を読み書きしようとしています。ロックは、ロック内の読み取りまたは変更されたメモリの一貫性が保証されることを保証し、ロックは、一度に1つのスレッドのみが特定のメモリチャンクにアクセスすることを保証します。ロックが遅すぎる状況の数は非常に少なく、正確なメモリモデルを理解していないためにコードを間違える可能性は非常に大きくなります。 Interlocked操作の最も些細な使用法を除き、低ロックコードを記述しようとはしません。 「揮発性」の使用法は本物の専門家に任せます。
詳細については、以下を参照してください。
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レジスタに割り当てています。かなり簡単です。ただし、興味深いビットがいくつかあります。leaebx、[ebx]命令は事実上マルチバイトnop命令であるため、ループは16バイトにアライメントされたメモリアドレスにジャンプします。もう1つは、inc eax命令を使用する代わりに、edxを使用してループカウンターをインクリメントする方法です。 add reg、reg命令は、inc reg命令と比較して、いくつかのIA32コアでレイテンシが低くなりますが、レイテンシが高くなることはありません。
次に、揮発性ループカウンターを使用したループについて説明します。カウンターは[esp]に格納され、volatileキーワードは、値が常にメモリーから読み取られ、メモリーに書き込まれ、レジスターに決して割り当てられないことをコンパイラーに伝えます。コンパイラーは、カウンター値を更新するときに3つの異なるステップ(load eax、inc eax、save eax)としてロード/インクリメント/ストアを実行しない限り、メモリが単一の命令で直接変更されます(add mem 、reg)。コードの作成方法により、ループカウンターの値は、単一のCPUコアのコンテキスト内で常に最新になります。データに対する操作は、破損またはデータの損失を招くことはありません(したがって、incの間に値が変化するため、load/inc/storeを使用しないため、ストアで失われます)。割り込みは現在の命令が完了してからしか処理できないため、アライメントされていないメモリがあってもデータが破損することはありません。
システムに2番目のCPUを導入すると、volatileキーワードは、同時に別のCPUによって更新されるデータを保護しません。上記の例では、潜在的な破損を取得するために、データのアライメントを解除する必要があります。 volatileキーワードは、データをアトミックに処理できない場合、潜在的な破損を防止しません。たとえば、ループカウンターがlong long(64ビット)型の場合、値を更新するには2つの32ビット操作が必要になります。割り込みが発生してデータが変更される可能性があります。
したがって、volatileキーワードは、操作が常にアトミックであるように、ネイティブレジスタのサイズ以下のアライメントされたデータにのみ適しています。
Volatileキーワードは、IO操作で使用されると考えられていました。この操作では、IOは常に変化しますが、メモリマップUARTデバイスなど、一定のアドレスを持ちます。コンパイラは、アドレスから読み取られた最初の値を再利用し続けるべきではありません。
大きなデータを処理する場合、または複数のCPUを使用する場合は、データアクセスを適切に処理するために、より高いレベル(OS)のロックシステムが必要になります。
.NET 1.1を使用している場合、ダブルチェックロックを実行するときにvolatileキーワードが必要です。どうして? .NET 2.0より前のバージョンでは、次のシナリオにより、2番目のスレッドがnullではないが完全に構築されたオブジェクトにアクセスする可能性があるためです。
.NET 2.0以前では、コンストラクタの実行が完了する前に、this.fooにFooの新しいインスタンスを割り当てることができました。この場合、2番目のスレッドが(スレッド1のFooのコンストラクターへの呼び出し中に)入ってくる可能性があり、以下が発生します。
.NET 2.0以前では、this.fooを揮発性として宣言して、この問題を回避できました。 .NET 2.0以降、ダブルチェックロックを実現するためにvolatileキーワードを使用する必要がなくなりました。
ウィキペディアには、実際にダブルチェックロックに関する優れた記事があり、このトピックについて簡単に触れています。 http://en.wikipedia.org/wiki/Double-checked_locking
場合によっては、コンパイラはフィールドを最適化し、レジスタを使用してフィールドを保存します。更新がレジスタ(メモリではなく)に格納されているため、スレッド1がフィールドに書き込みを行い、別のスレッドがそれにアクセスすると、2番目のスレッドは古いデータを取得します。
Volatileキーワードは、コンパイラに「この値をメモリに保存してほしい」と言っていると考えることができます。これにより、2番目のスレッドが最新の値を取得することが保証されます。
From MSDN :volatile修飾子は通常、lockステートメントを使用せずにアクセスをシリアル化することなく、複数のスレッドによってアクセスされるフィールドに使用されます。 volatile修飾子を使用すると、あるスレッドが別のスレッドによって書き込まれた最新の値を取得することが保証されます。
CLRは命令を最適化するのが好きなので、コードでフィールドにアクセスするとき、フィールドの現在の値に常にアクセスするとは限りません(スタックなどから)。フィールドをvolatile
としてマークすると、フィールドの現在の値が命令によって確実にアクセスされます。これは、プログラム内の同時スレッドまたはオペレーティングシステムで実行されている他のコードによって値が(非ロックシナリオで)変更できる場合に役立ちます。
明らかに最適化はいくらか失われますが、コードがよりシンプルになります。
したがって、このすべてをまとめると、質問に対する正しい答えは次のとおりです。コードが2.0ランタイム以降で実行されている場合、volatileキーワードはほとんど必要とされず、不必要に使用された場合は良いよりも害が大きくなります。 I.E.使用しないでください。しかし、ランタイムの以前のバージョンでは、ISは静的フィールドの適切なダブルチェックロックに必要でした。具体的には、クラスに静的クラス初期化コードがある静的フィールド。
コンパイラは、コード内のステートメントの順序を変更して最適化することがあります。通常、これはシングルスレッド環境では問題ではありませんが、マルチスレッド環境では問題になる可能性があります。次の例を参照してください。
private static int _flag = 0;
private static int _value = 0;
var t1 = Task.Run(() =>
{
_value = 10; /* compiler could switch these lines */
_flag = 5;
});
var t2 = Task.Run(() =>
{
if (_flag == 5)
{
Console.WriteLine("Value: {0}", _value);
}
});
T1とt2を実行する場合、結果として出力または「値:10」は期待できません。コンパイラがt1関数内で行を切り替える可能性があります。 t2が実行された場合、_flagの値は5であるが、_valueの値は0である可能性があります。したがって、予期されるロジックが壊れる可能性があります。
これを修正するには、フィールドに適用できるvolatileキーワードを使用できます。このステートメントはコンパイラーの最適化を無効にするため、コードの正しい順序を強制できます。
private static volatile int _flag = 0;
volatileは、本当に必要な場合にのみ使用してください。特定のコンパイラの最適化が無効になるため、パフォーマンスが低下します。また、すべての.NET言語でサポートされているわけではないため(Visual Basicはサポートしていません)、言語の相互運用性を妨げます。