複数のテキストは、.NETでダブルチェックロックを実装する場合、ロックするフィールドにvolatile修飾子を適用する必要があると述べています。しかし、なぜ正確に?次の例を検討してください。
public sealed class Singleton
{
private static volatile Singleton instance;
private static object syncRoot = new Object();
private Singleton() {}
public static Singleton Instance
{
get
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
}
「ロック(syncRoot)」が必要なメモリの一貫性を達成しないのはなぜですか? 「lock」ステートメントの後、読み取りと書き込みの両方が揮発性になり、必要な一貫性が達成されるというのは本当ではありませんか?
揮発性は不要です。まあ、ちょっと**
volatile
は、変数の読み取りと書き込みの間にメモリバリア*を作成するために使用されます。lock
を使用すると、ブロックへのアクセスを1つのスレッドに制限することに加えて、lock
内のブロックの周りにメモリバリアが作成されます。
メモリバリアにより、各スレッドが変数の最新値(レジスタにキャッシュされたローカル値ではない)を読み取り、コンパイラーがステートメントの順序を変更しないようにします。すでにロックを取得しているため、volatile
を使用する必要はありません**。
Joseph Albahari は、このことをこれまで以上にうまく説明しています。
そして、C#でJon Skeetの シングルトンの実装ガイド を必ずチェックしてください。
更新:
* volatile
により、変数の読み取りはVolatileRead
sになり、書き込みはVolatileWrite
sになります。これは、x86およびCLRのx64では、MemoryBarrier
。他のシステムではより細かくなります。
**私の答えは、x86およびx64プロセッサでCLRを使用している場合にのみ正しいです。 Itmightは、Mono(および他の実装)、Itanium64、および将来のハードウェアなど、他のメモリモデルでも当てはまります。これは、ジョンがダブルチェックロックに関する「落とし穴」の記事で言及していることです。
弱いメモリモデルの状況でコードが適切に機能するには、{変数をvolatile
としてマークする、Thread.VolatileRead
で読み取る、またはThread.MemoryBarrier
への呼び出しを挿入する必要があります。 。
私が理解していることから、CLRでは(IA64でも)、書き込みは並べ替えられません(書き込みは常にリリースセマンティクスを持ちます)。ただし、IA64では、読み取りに揮発性のマークが付けられていない限り、読み取りは書き込みの前に並べ替えられます。残念なことに、私はIA64ハードウェアにアクセスすることができないため、私が言うことは推測にすぎません。
私もこれらの記事が役に立ちました:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
ヴァンス・モリソンの記事 (これに関連するものはすべて、二重チェックのロックについて説明しています)
クリス・ブルームの記事 (これへのすべてのリンク)
ジョーダフィー:ダブルチェックロックの壊れたバリアント
マルチスレッドに関するluis abreuのシリーズは、概念の概要も説明しています。
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http://msmvps.com/blogs/luisabreu/archive/2009/07/03/multithreading-introducing-memory-fences.aspx
volatile
フィールドなしで実装する方法があります。説明します...
危険なのは、ロック内のメモリアクセスの再順序付けであり、ロック外では完全に初期化されていないインスタンスを取得できると思います。これを避けるために、私はこれを行います:
public sealed class Singleton
{
private static Singleton instance;
private static object syncRoot = new Object();
private Singleton() {}
public static Singleton Instance
{
get
{
// very fast test, without implicit memory barriers or locks
if (instance == null)
{
lock (syncRoot)
{
if (instance == null)
{
var temp = new Singleton();
// ensures that the instance is well initialized,
// and only then, it assigns the static variable.
System.Threading.Thread.MemoryBarrier();
instance = temp;
}
}
}
return instance;
}
}
}
シングルトンクラスのコンストラクター内にいくつかの初期化コードがあることを想像してください。フィールドに新しいオブジェクトのアドレスを設定した後にこれらの命令を並べ替えると、インスタンスが不完全になります...クラスに次のコードがあると想像してください。
private int _value;
public int Value { get { return this._value; } }
private Singleton()
{
this._value = 1;
}
ここで、new演算子を使用したコンストラクターの呼び出しを想像してください。
instance = new Singleton();
これは、次の操作に拡張できます。
ptr = allocate memory for Singleton;
set ptr._value to 1;
set Singleton.instance to ptr;
これらの指示を次のように並べ替えるとどうなりますか:
ptr = allocate memory for Singleton;
set Singleton.instance to ptr;
set ptr._value to 1;
違いはありますか? [〜#〜] no [〜#〜]単一のスレッドを考える場合。 [〜#〜] yes [〜#〜]複数のスレッドを考えている場合...スレッドがset instance to ptr
:
ptr = allocate memory for Singleton;
set Singleton.instance to ptr;
-- thread interruped here, this can happen inside a lock --
set ptr._value to 1; -- Singleton.instance is not completelly initialized
これは、メモリアクセスの並べ替えを許可しないことにより、メモリバリアが回避するものです。
ptr = allocate memory for Singleton;
set temp to ptr; // temp is a local variable (that is important)
set ptr._value to 1;
-- memory barrier... cannot reorder writes after this point, or reads before it --
-- Singleton.instance is still null --
set Singleton.instance to temp;
ハッピーコーディング!
questionに実際に答えた人はいないと思うので、試してみます。
Volatileと最初のif (instance == null)
は「必要」ではありません。ロックにより、このコードはスレッドセーフになります。
質問は次のとおりです。最初のif (instance == null)
を追加する理由は何ですか?
理由はおそらく、コードのロックされたセクションを不必要に実行することを避けるためです。ロック内でコードを実行している間、そのコードも実行しようとする他のスレッドはブロックされ、多くのスレッドからシングルトンに頻繁にアクセスしようとすると、プログラムの速度が低下します。言語/プラットフォームによっては、ロック自体のオーバーヘッドも回避したい場合があります。
したがって、ロックが必要かどうかを確認するための非常に迅速な方法として、最初のヌルチェックが追加されます。シングルトンを作成する必要がない場合は、ロックを完全に回避できます。
ただし、何らかの方法でロックせずに参照がnullであるかどうかを確認することはできません。プロセッサのキャッシュにより、別のスレッドがそれを変更し、不必要にロックに入る「古い」値を読み取る可能性があるためです。しかし、ロックを回避しようとしています!
そのため、ロックを使用する必要なく、シングルトンを揮発性にして、最新の値を確実に読み取るようにします。
Volatileは変数への1回のアクセス中にのみ保護するため、内部ロックが必要です。ロックを使用せずに安全にテストおよび設定することはできません。
さて、これは実際に便利ですか?
まあ、私は「ほとんどの場合、いいえ」と言うでしょう。
Singleton.Instanceがロックのために非効率を引き起こす可能性がある場合、なぜこれを重大な問題になるほど頻繁に呼び出すのか?シングルトンの重要な点は1つしかないため、コードはシングルトン参照を1回読み取ってキャッシュできることです。
このキャッシュが不可能になる唯一のケースは、多数のスレッドがある場合です(たとえば、すべてのリクエストを処理するために新しいスレッドを使用するサーバーは、それぞれがSingleton.Instanceを1回呼び出す必要があります)。
だから私は、ダブルチェックロックが非常に特定のパフォーマンスクリティカルなケースで本当の場所を持っているメカニズムであると疑っています、そして誰もが実際にそれが何をするのか、それを考えないで「これを行うための適切な方法です」実際に使用する場合に必要になります。
私の知る限り(そして、これに注意してください、私は多くの並行処理を行っていません)いいえ。ロックは、複数の競合(スレッド)間の同期を提供するだけです。
一方、volatileは、キャッシュされた(そして間違った)値につまずかないように、毎回値を再評価するようにマシンに指示します。
http://msdn.Microsoft.com/en-us/library/ms998558.aspx を参照し、次の引用に注意してください。
また、インスタンス変数にアクセスする前にインスタンス変数への割り当てが完了するように、変数は揮発性であると宣言されます。
Volatileの説明: http://msdn.Microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx
ダブルチェックロックパターンではvolatileを使用する必要があります。
ほとんどの人は、揮発性を必要としない証拠としてこの記事を指します: https://msdn.Microsoft.com/en-us/magazine/cc163715.aspx#S1
しかし、彼らは最後まで読むことができません: "警告の最後の言葉-私は既存のプロセッサで観察された動作からx86メモリモデルを推測しているだけです。ハードウェアとコンパイラは、時間の経過とともにより攻撃的になる可能性があります。この脆弱性がコードに与える影響を最小限に抑えるための戦略を次に示します。暗黙の保証に頼る代わりに、揮発性宣言を使用します。 "
さらに説得力が必要な場合は、他のプラットフォームで使用されるECMA仕様に関する次の記事をお読みください:msdn.Microsoft.com/en-us/magazine/jj863136.aspx
さらに説得力が必要な場合は、この新しい記事を読んで、揮発性なしで動作しないように最適化を行うことができます:msdn.Microsoft.com/en-us/magazine/jj883956.aspx
要約すると、一時的にvolatileを使用しなくても「動作する」可能性がありますが、適切なコードを記述してvolatileまたはvolatileread/writeメソッドを使用することはありません。そうしないと示唆する記事では、コードに影響を与える可能性のあるJIT /コンパイラーの最適化の潜在的なリスクの一部を除外している場合があります。また、前回の記事で述べたように、volatileなしで動作するという以前の仮定はすでにARMに当てはまらない可能性があります。
lock
で十分です。 MS言語仕様(3.0)自体は、§8.12でこの正確なシナリオに言及していますが、volatile
には言及していません。
より良いアプローチは、プライベート静的オブジェクトをロックすることにより、静的データへのアクセスを同期することです。例えば:
class Cache { private static object synchronizationObject = new object(); public static void Add(object x) { lock (Cache.synchronizationObject) { ... } } public static void Remove(object x) { lock (Cache.synchronizationObject) { ... } } }
私が探していたものを見つけたと思う。詳細はこの記事にあります- http://msdn.Microsoft.com/en-us/magazine/cc163715.aspx#S1 。
まとめると、この状況では.NET volatile修飾子は実際には必要ありません。ただし、弱いメモリモデルでは、遅延開始オブジェクトのコンストラクターでの書き込みがフィールドへの書き込み後に遅延する可能性があるため、他のスレッドが最初のifステートメントで破損した非nullインスタンスを読み取る可能性があります。