volatile
キーワードに関するいくつかの記事を読みましたが、正しい使用法がわかりませんでした。 C#およびJavaで何に使用すべきか教えてください。
C#とJavaの両方について、「volatile」は、変数の値がプログラム自体の範囲外で変更される可能性があるため、変数の値をキャッシュしてはならないことをコンパイラに伝えます。コンパイラは、変数が「制御外」に変更された場合に問題を引き起こす可能性のある最適化を回避します。
この例を考えてみましょう:
int i = 5;
System.out.println(i);
コンパイラは、これを最適化して、次のように5を出力するだけです。
System.out.println(5);
ただし、i
を変更できる別のスレッドがある場合、これは間違った動作です。別のスレッドがi
を6に変更しても、最適化されたバージョンは5を出力します。
volatile
キーワードは、このような最適化とキャッシュを防止するため、変数を別のスレッドで変更できる場合に役立ちます。
Volatileが変数に対して行うことを理解するには、変数がvolatileではないときに何が起こるかを理解することが重要です。
2つのスレッドAとBが不揮発性変数にアクセスしている場合、各スレッドは変数のローカルコピーをローカルキャッシュに保持します。ローカルキャッシュ内のスレッドAによって行われた変更は、スレッドBには表示されません。
変数がvolatileと宣言されている場合、それは本質的に、スレッドがそのような変数をキャッシュしてはならないこと、つまり、スレッドがメインメモリから直接読み取られない限り、これらの変数の値を信頼しないことを意味します。
つまり、変数をいつ揮発性にするか
多くのスレッドがアクセスできる変数があり、値がプログラムの他のスレッド/プロセス/外部によって更新された場合でも、すべてのスレッドにその変数の最新の更新値を取得させたい場合。
volatileキーワード は、JavaとC#の両方で異なる意味を持ちます。
Java言語仕様 から:
フィールドはvolatileと宣言される場合があります。その場合、Javaメモリモデルは、すべてのスレッドが変数の一貫した値を見るようにします。
volatileキーワード のC#リファレンスから:
Volatileキーワードは、オペレーティングシステム、ハードウェア、または同時実行スレッドなどによってプログラム内のフィールドを変更できることを示します。
揮発性フィールドの読み取りにはacquire意味があります。つまり、揮発性変数からのメモリ読み取りは、後続のメモリ読み取りの前に発生することが保証されます。コンパイラーが並べ替えを実行するのをブロックし、ハードウェアがそれを必要とする場合(CPUの順序が弱い)、特別な命令を使用して、揮発性読み取りの後に発生するが投機的に早期に開始された読み取りをハードウェアにフラッシュさせます。負荷獲得の問題とその廃止の間に投機的な負荷が発生するのを防ぐことにより、そもそもそれらが早期に発行されるのを防ぎます。
Volatileフィールドの書き込みにはreleaseセマンティクスがあります。つまり、揮発性変数へのメモリ書き込みは、以前のすべてのメモリ書き込みが他のプロセッサから見えるようになるまで遅延することが保証されます。
次の例を考えてみましょう。
something.foo = new Thing();
foo
がクラスのメンバー変数であり、他のCPUがsomething
によって参照されるオブジェクトインスタンスにアクセスできる場合、値foo
change beforeThing
コンストラクターのメモリ書き込みはグローバルに可視です!これが「弱く順序付けられたメモリ」の意味です。これは、コンパイラがfoo
へのストアの前にコンストラクタ内のすべてのストアを持っている場合でも発生する可能性があります。 foo
がvolatile
の場合、foo
へのストアにはリリースセマンティクスがあり、ハードウェアはfoo
への書き込み前のすべての書き込みが表示されることを保証します。 foo
への書き込みを許可する前に、他のプロセッサに送信します。
foo
への書き込みがひどく並べ替えられるのはどうしてですか? foo
を保持するキャッシュラインがキャッシュ内にあり、コンストラクター内のストアがキャッシュをミスした場合、キャッシュへの書き込みがミスするよりもはるかに早くストアが完了する可能性があります。
Intelの(ひどい)Itaniumアーキテクチャでは、メモリの順序が弱かった。元のXBox 360で使用されていたプロセッサのメモリの順序が弱かった。非常に人気のあるARMv7-Aを含む多くのARMプロセッサは、メモリの順序が弱いです。
開発者は、ロックのようなものが完全なメモリバリアを実行するため、これらのデータの競合を頻繁に見ません。ロック内のロードは、ロックが取得される前に投機的に実行することはできません。ロックが取得されるまで遅延されます。ロックを解除してストアを遅らせることはできません。ロック内で行われたすべての書き込みがグローバルに表示されるまで、ロックを解除する命令は遅延します。
より完全な例は、「ダブルチェックロック」パターンです。このパターンの目的は、オブジェクトを遅延初期化するために常にロックを取得する必要を回避することです。
ウィキペディアから引っかかった:
public class MySingleton {
private static object myLock = new object();
private static volatile MySingleton mySingleton = null;
private MySingleton() {
}
public static MySingleton GetInstance() {
if (mySingleton == null) { // 1st check
lock (myLock) {
if (mySingleton == null) { // 2nd (double) check
mySingleton = new MySingleton();
// Write-release semantics are implicitly handled by marking mySingleton with
// 'volatile', which inserts the necessary memory barriers between the constructor call
// and the write to mySingleton. The barriers created by the lock are not sufficient
// because the object is made visible before the lock is released.
}
}
}
// The barriers created by the lock are not sufficient because not all threads will
// acquire the lock. A fence for read-acquire semantics is needed between the test of mySingleton
// (above) and the use of its contents.This fence is automatically inserted because mySingleton is
// marked as 'volatile'.
return mySingleton;
}
}
この例では、MySingleton
コンストラクター内のストアは、mySingleton
へのストアの前に他のプロセッサーから見えない場合があります。その場合、mySingletonを覗く他のスレッドはロックを取得せず、コンストラクターへの書き込みを必ずしも取得しません。
volatile
はキャッシュを妨げません。それが行うことは、他のプロセッサが書き込みを「見る」順序を保証することです。ストアのリリースは、保留中の書き込みがすべて完了し、関連するラインがキャッシュされている場合にキャッシュラインを破棄/書き戻すように他のプロセッサに指示するバスサイクルが発行されるまでストアを遅延させます。ロード獲得は、推測された読み取りをフラッシュし、それらが過去の古い値にならないようにします。
Javaでは、「volatile」を使用して、複数のスレッドが同時に変数を使用できることをJVMに伝えるため、特定の一般的な最適化を適用できません。
特に、同じ変数にアクセスする2つのスレッドが同じマシンの別々のCPUで実行されている状況。メモリアクセスはキャッシュアクセスよりも非常に遅いため、CPUが保持するデータを積極的にキャッシュすることは非常に一般的です。これは、データがCPU1で更新の場合、キャッシュが自身をクリアすることを決定する代わりに、すべてのキャッシュをすぐにメインメモリに移動する必要があるため、CPU2が更新された値を見ることができるようにします途中のすべてのキャッシュ)。
不揮発性のデータを読み取る場合、実行中のスレッドは常に更新された値を取得する場合と取得しない場合があります。ただし、オブジェクトが揮発性の場合、スレッドは常に最新の値を取得します。