回答:
私はこれに答えるのにEric Lippertよりも優れた人はいないと思います(オリジナルの強調):
C#では、「揮発性」は「コンパイラーとジッターがコードの並べ替えやレジスターのキャッシュの最適化をこの変数で実行しないことを確認する」だけではありません。また、「他のプロセッサを停止してメインメモリとキャッシュを同期させる場合でも、最新の値を確実に読み取るために必要な処理をすべてプロセッサに指示する」ことも意味します。
実際には、その最後のビットは嘘です。揮発性の読み取りと書き込みの実際のセマンティクスは、ここで説明したよりもかなり複雑です。実際に彼らは実際にすべてのプロセッサは、それが何をしているか停止していることを保証するものではありません、メインメモリから/へのキャッシュをして更新されます。むしろ、それらは、読み取りと書き込みの前後のメモリアクセスが互いに対して順序付けられていることが観察される方法について、より弱い保証を提供します。新しいスレッドの作成、ロックの入力、Interlockedファミリのメソッドの使用などの特定の操作は、順序の監視についてより強力な保証をもたらします。詳細が必要な場合は、C#4.0仕様のセクション3.10および10.5.3をお読みください。
率直に言って、揮発性フィールドを作成することはお勧めしません。揮発性フィールドは、何かがおかしくなっている兆候です。ロックを設定せずに、2つの異なるスレッドで同じ値を読み書きしようとしています。ロックは、ロック内で読み取られた、または変更されたメモリの一貫性が保証されることを保証します。ロックは、一度に1つのスレッドのみが特定のメモリチャンクにアクセスすることを保証します。ロックが遅すぎる状況の数は非常に少なく、正確なメモリモデルを理解していないためにコードが誤って取得される可能性は非常に大きくなります。Interlockedオペレーションの最も簡単な使用法を除いて、ローロックコードを記述しようとはしません。「揮発性」の使用法は本当の専門家に任せます。
詳細については、以下を参照してください。
volatile
はロックのおかげでそこにあります
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が絶えず変化するが、メモリマップされたUARTデバイスなどの一定のアドレスを持つIO操作で使用するために考えられ、コンパイラーはアドレスから読み取られた最初の値を再利用し続けるべきではありません。
大きなデータを処理している場合や複数の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
foo
。スレッド1はロックしていないthis.bar
ので、スレッド1だけが特定の時点でfooを初期化できますか?私はロックが再び解放された後、あなたが値をチェックしますか、意味、とにかくそれはスレッド1から新たな価値を持っている必要がある場合
Joydip Kanjilalによるこの記事はとても役に立ちました!
When you mark an object or a variable as volatile, it becomes a candidate for volatile reads and writes. It should be noted that in C# all memory writes are volatile irrespective of whether you are writing data to a volatile or a non-volatile object. However, the ambiguity happens when you are reading data. When you are reading data that is non-volatile, the executing thread may or may not always get the latest value. If the object is volatile, the thread always gets the most up-to-date value
参照用にここに残しておきます
コンパイラーは、コード内のステートメントの順序を変更して最適化することがあります。通常、これはシングルスレッド環境では問題になりませんが、マルチスレッド環境では問題になる可能性があります。次の例を参照してください。
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ではサポートされていません)、言語の相互運用性が妨げられます。
複数のスレッドが変数にアクセスできます。最新の更新は変数にあります