C#でvolatileキーワードを使用する必要があるのはいつですか?


299

誰かがC#の揮発性キーワードの良い説明を提供できますか?それはどの問題を解決し、どの問題を解決しませんか?どのような場合にロックを使用する必要がなくなりますか?


6
ロックの使用を節約したいのはなぜですか?競合しないロックは、プログラムに数ナノ秒を追加します。本当に数ナノ秒の余裕がありませんか?
Eric Lippert 2013

回答:


273

私はこれに答えるのにEric Lippertよりも優れた人はいないと思います(オリジナルの強調):

C#では、「揮発性」は「コンパイラーとジッターがコードの並べ替えやレジスターのキャッシュの最適化をこの変数で実行しないことを確認する」だけではありません。また、「他のプロセッサを停止してメインメモリとキャッシュを同期させる場合でも、最新の値を確実に読み取るために必要な処理をすべてプロセッサに指示する」ことも意味します。

実際には、その最後のビットは嘘です。揮発性の読み取りと書き込みの実際のセマンティクスは、ここで説明したよりもかなり複雑です。実際に彼らは実際にすべてのプロセッサは、それが何をしているか停止していることを保証するものではありません、メインメモリから/へのキャッシュをして更新されます。むしろ、それらは、読み取りと書き込みの前後のメモリアクセスが互いに対して順序付けられていることが観察される方法について、より弱い保証を提供します。新しいスレッドの作成、ロックの入力、Interlockedファミリのメソッドの使用などの特定の操作は、順序の監視についてより強力な保証をもたらします。詳細が必要な場合は、C#4.0仕様のセクション3.10および10.5.3をお読みください。

率直に言って、揮発性フィールドを作成することはお勧めしません。揮発性フィールドは、何かがおかしくなっている兆候です。ロックを設定せずに、2つの異なるスレッドで同じ値を読み書きしようとしています。ロックは、ロック内で読み取られた、または変更されたメモリの一貫性が保証されることを保証します。ロックは、一度に1つのスレッドのみが特定のメモリチャンクにアクセスすることを保証します。ロックが遅すぎる状況の数は非常に少なく、正確なメモリモデルを理解していないためにコードが誤って取得される可能性は非常に大きくなります。Interlockedオペレーションの最も簡単な使用法を除いて、ローロックコードを記述しようとはしません。「揮発性」の使用法は本当の専門家に任せます。

詳細については、以下を参照してください。


29
できればこれに反対票を投じます。そこには多くの興味深い情報がありますが、それは実際には彼の質問には答えません。ロックに関連するvolatileキーワードの使用について質問しています。しばらくの間(2.0 RT以前)、フィールドインスタンスにコンストラクター内の初期化コードがある場合に静的フィールドをスレッドセーフにするために、volatileキーワードを使用する必要がありました(AndrewTekの回答を参照)。本番環境にはまだ1.1 RTコードがたくさんあります。それを維持している開発者は、そのキーワードが存在する理由と削除しても安全かどうかを知っている必要があります。
ポールイースター

3
@PaulEaster (通常はシングルトンパターンで)がチェックされるロックに使用できるということ、そうである必要があるという意味ではありません。.NETメモリモデルに依存することはおそらく悪い習慣です。代わりにECMAモデルに依存する必要があります。たとえば、モデルが異なる可能性があるモノに1日移植することができます。また、ハードウェアのアーキテクチャが異なれば、状況が変わる可能性があることも理解しています。詳細については、stackoverflow.com/ a/ 7230679 / 67824を参照してください。(すべての.NETバージョンの)より良いシングルトンの選択肢については以下を参照してくださいcsharpindepth.com/articles/general/singleton.aspx
オハッドシュナイダー

6
言い換えると、質問に対する正解は次のとおりです。コードが2.0ランタイム以降で実行されている場合、volatileキーワードはほとんど必要なく、不必要に使用すると、害を及ぼします。ただし、ランタイムの以前のバージョンでは、静的フィールドの適切なダブルチェックロックに必要です。
ポールイースター

3
これは、ロックと揮発性変数が次の意味で相互に排他的であることを意味します:ある変数の周りでロックを使用した場合、その変数を揮発性として宣言する必要はもうありませんか?
giorgim

4
@Giorgiはい-によって保証されたメモリの障壁volatileはロックのおかげでそこにあります
Ohad Schneider

54

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)のロックシステムが必要です。


これはC ++ですが、原則はC#に適用されます。
Skizz

6
Eric Lippertは、C ++ではvolatileがコンパイラーによる最適化の実行を妨げるだけであると書いていますが、C#volatileでは、他のコア/プロセッサー間の通信を行って、最新の値が確実に読み取られるようにしています。
Peter Huber 2017年

44

.NET 1.1を使用している場合は、ダブルチェックロックを行うときにvolatileキーワードが必要です。どうして?.NET 2.0より前のバージョンでは、次のシナリオでは、2番目のスレッドがnullではないが完全に構築されていないオブジェクトにアクセスする可能性があります。

  1. スレッド1は、変数がnullかどうかを尋ねます。//if(this.foo == null)
  2. スレッド1は変数がnullであると判断し、ロックに入ります。//lock(this.bar)
  3. スレッド1は、変数がnullかどうかをAGAINに尋ねます。//if(this.foo == null)
  4. スレッド1はまだ変数がnullであると判断しているため、コンストラクターを呼び出して変数に値を割り当てます。//this.foo = new Foo();

.NET 2.0より前のバージョンでは、コンストラクタの実行が完了する前に、this.fooにFooの新しいインスタンスを割り当てることができました。この場合、2番目のスレッドが入り(スレッド1がFooのコンストラクターを呼び出す間に)、次のようなことが起こります。

  1. スレッド2は変数がnullかどうかを尋ねます。//if(this.foo == null)
  2. スレッド2は変数がnullではないと判断するため、変数を使用しようとします。//this.foo.MakeFoo()

.NET 2.0より前は、this.fooを揮発性として宣言して、この問題を回避できました。.NET 2.0以降、ダブルチェックロックを実現するためにvolatileキーワードを使用する必要がなくなりました。

ウィキペディアには実際にダブルチェックロックについての良い記事があり、このトピックについて簡単に触れています:http : //en.wikipedia.org/wiki/Double-checked_locking


2
これはまさに私がレガシーコードで見たものであり、それについて疑問に思っていました。だから私はより深い研究を始めました。ありがとう!
Peter Porfy 2014年

1
スレッド2がどのように値を割り当てるのかわかりませんfoo。スレッド1はロックしていないthis.barので、スレッド1だけが特定の時点でfooを初期化できますか?私はロックが再び解放された後、あなたが値をチェックしますか、意味、とにかくそれはスレッド1から新たな価値を持っている必要がある場合
gilmishal

24

場合によっては、コンパイラーはフィールドを最適化し、レジスターを使用してそれを保管します。スレッド1がフィールドに書き込みを行い、別のスレッドがフィールドにアクセスした場合、更新はメモリではなくレジスタに格納されているため、2番目のスレッドは古いデータを取得します。

volatileキーワードは、コンパイラに「この値をメモリに格納してほしい」と言っていると考えることができます。これにより、2番目のスレッドが最新の値を取得することが保証されます。


21

MSDNから:volatile修飾子は通常、lockステートメントを使用せずに複数のスレッドによってアクセスされるフィールドに使用され、アクセスをシリアル化します。volatile修飾子を使用すると、1つのスレッドが別のスレッドによって書き込まれた最新の値を確実に取得できます。


13

CLRは命令を最適化するのが好きなので、コードでフィールドにアクセスするとき、フィールドの現在の値に常にアクセスするとは限りません(スタックからなど)。フィールドにマークを付けると、フィールドvolatileの現在の値が命令によってアクセスされることが保証されます。これは、プログラム内の並行スレッドまたはオペレーティングシステムで実行されている他のコードによって値が(非ロックシナリオで)変更される可能性がある場合に役立ちます。

明らかに一部の最適化が失われますが、コードがより単純なままになります。


3

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

参照用にここに残しておきます


0

コンパイラーは、コード内のステートメントの順序を変更して最適化することがあります。通常、これはシングルスレッド環境では問題になりませんが、マルチスレッド環境では問題になる可能性があります。次の例を参照してください。

 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ではサポートされていません)、言語の相互運用性が妨げられます。


2
あなたの例は本当に悪いです。プログラマーは、t1のコードが最初に書かれているという事実に基づいて、t2タスクの_flagの値を期待するべきではありません。最初に書き込まれます!=最初に実行されます。コンパイラーがt1でこれらの2行を切り替えるかどうかは関係ありません。コンパイラがこれらのステートメントを切り替えなかった場合でも、_flagのvolatileキーワードを使用しても、elseブランチのConsole.WriteLneは実行されます。
Jakotheshadows 2016年

@jakotheshadows、あなたは正しい、私は私の答えを編集しました。私の主なアイデアは、t1とt2を同時に実行すると予想されるロジックが壊れる可能性があることを示すことでした
Aliaksei Maniuk

0

つまり、これらすべてをまとめると、質問に対する正しい答えは次のとおりです。コードが2.0ランタイム以降で実行されている場合、volatileキーワードはほとんど必要なく、不必要に使用すると害が大きくなります。IEこれを使用しないでください。しかし、ランタイムの以前のバージョンでは、静的フィールドの適切なダブルチェックロックに必要です。具体的には、クラスに静的クラス初期化コードがある静的フィールド。


-4

複数のスレッドが変数にアクセスできます。最新の更新は変数にあります

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