.NETのダブルチェックロックでの揮発性修飾子の必要性


85

複数のテキストによると、.NETでダブルチェックロックを実装する場合、ロックしているフィールドには揮発性修飾子を適用する必要があります。しかし、なぜ正確に?次の例を検討します。

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」ステートメントの後、読み取りと書き込みの両方が揮発性になり、必要な一貫性が達成されるというのは本当ではありませんか?


2
これはすでに何度も噛まれています。 yoda.arachsys.com/csharp/singleton.html
Hans Passant

1
残念ながら、その記事では、Jonは「volatile」を2回参照しており、どちらの参照も彼が提供したコード例を直接参照していません。
ダンエスパルザ2011

懸念事項を理解するには、次の記事を参照してください。igoro.com / archive / volatile-keyword-in-c-memory-model-explained 基本的に、JITがインスタンス変数にCPUレジスタを使用することは理論的には可能です。特に、そこに少し余分なコード。したがって、ifステートメントを2回実行すると、別のスレッドで変更されても、同じ値が返される可能性があります。実際には、答えは少し複雑です。ロックステートメントがここで物事を改善する責任がある場合とない場合があります(続き)
user2685937 2017年

(前のコメントの続きを参照)-これが実際に起こっていると思うことです-基本的に、変数の読み取りまたは設定よりも複雑なことを行うコードは、JITに言うようにトリガーする可能性があります。これを最適化しようとするのを忘れて、メモリにロードして保存しましょう。関数が呼び出された場合、JITは、毎回メモリから直接書き込みおよび読み取りを行うのではなく、レジスタを保存するたびにレジスタを保存および再ロードする必要がある可能性があります。ロックが特別なものではないことをどうやって知ることができますか?Igorからの前のコメントに投稿したリンクを見てください(次のコメントに続く)
user2685937 2017年

(上記の2つのコメントを参照)-Igorのコードをテストし、新しいスレッドを作成するときに、その周りにロックを追加し、ループさせました。インスタンス変数がループから引き上げられたため、コードが終了することはありませんでした。whileループに追加すると、単純なローカル変数セットによって変数がループから引き上げられます。ifステートメントやメソッド呼び出し、またはロック呼び出しでさえも最適化が妨げられ、機能するようになります。そのため、複雑なコードでは、JITに最適化させるのではなく、直接変数アクセスを強制することがよくあります。(次のコメントに続く)
user2685937 2017年

回答:


60

揮発性は不要です。まあ、一種の**

volatile変数の読み取りと書き込みの間にメモリバリア*を作成するために使用されます。
lockを使用lockすると、ブロックへのアクセスを1つのスレッドに制限するだけでなく、内部のブロックの周囲にメモリバリアが作成されます。
メモリバリアにより、各スレッドが変数の最新の値(一部のレジスタにキャッシュされているローカル値ではない)を読み取り、コンパイラがステートメントを並べ替えないようにします。volatileすでにロックを取得しているため、使用する必要はありません**。

ジョセフ・アルバハリは、私がこれまでにできたよりもずっとうまくこのことを説明しています。

また、C#でシングルトンを実装するためのJonSkeetのガイドを必ず確認してください。


update
*volatile変数の読み取りをVolatileReadsにし、書き込みをVolatileWritesにします。これは、CLRのx86およびx64ではMemoryBarrier。で実装されます。他のシステムでは、よりきめ細かい場合があります。

**私の答えは、x86およびx64プロセッサでCLRを使用している場合にのみ正しいです。これ、Mono(および他の実装)、Itanium64、および将来のハードウェアなど、他のメモリモデルにも当てはまる可能性があります。これは、ジョンがダブルチェックロックの「落とし穴」の記事で言及していることです。

弱いメモリモデルの状況でコードが正しく機能するためには、{変数をとしてマークする、変数をvolatile読み取るThread.VolatileRead、またはThread.MemoryBarrier}の呼び出しを挿入する}のいずれかを実行する必要がある場合があります。

私が理解していることから、CLR(IA64でも)では、書き込みは決して並べ替えられません(書き込みには常にリリースセマンティクスがあります)。ただし、IA64では、揮発性とマークされていない限り、読み取りは書き込みの前に来るように並べ替えることができます。残念ながら、私はIA64ハードウェアにアクセスして遊ぶことができないので、それについて私が言うことは何でも推測になります。

私はこれらの記事も役に立ちました:
http//www.codeproject.com/KB/tips/MemoryBarrier.aspx
vance morrisonの記事(これへのすべてのリンク、ダブルチェックロックについて説明しています)
chris brummeの記事 (これへのすべてのリンク)
Joe Duffy:ダブルチェックロックの壊れたバリアント

マルチスレッドに関するluisabreuのシリーズでは、概念の概要も説明しています
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


Jon Skeetは実際には、適切なメモリバリアを作成するには揮発性修飾子が必要であると述べていますが、最初のリンク作成者はロック(Monitor.Enter)で十分であると述べています。誰が実際に正しいですか?
コンスタンチン

@KonstantinJonはItanium64プロセッサのメモリモデルを参照していたようです。その場合、volatileを使用する必要があるかもしれません。ただし、x86およびx64プロセッサではvolatileは不要です。もう少し更新します。
dan

ロックが実際にメモリバリアを作成していて、メモリバリアが実際に命令の順序とキャッシュの無効化の両方に関するものである場合、すべてのプロセッサで機能するはずです。とにかく、そのような基本的なことが非常に多くの混乱を引き起こすのはとても奇妙です...
Konstantin

2
この答えは私には間違っているように見えます。どのプラットフォームvolatile不要な場合は、JITがそのプラットフォームでのメモリ負荷object s1 = syncRoot; object s2 = syncRoot;を最適化できなかったことを意味object s1 = syncRoot; object s2 = s1;します。それは私には非常にありそうもないようです。
user541686 2013

1
CLRが書き込みを並べ替えない場合でも(そうすることで多くの非常に優れた最適化を実行できるとは思えません)、コンストラクター呼び出しをインライン化してオブジェクトをインプレースで作成できる限り、バグがあります。 (半分初期化されたオブジェクトを見ることができました)。独立した任意のメモリ・モデルの基本となるCPUを使っています!エリックリペットコンストラクタの後membarrierは、その最適化を拒否したが、それは仕様で必須ではありませんし、私は、例えば、ARM上で起こって同じことを当てにしません..少なくとも紹介でインテルのCLRによると
VOO

34

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;
      }
   }
}

コードを理解する

Singletonクラスのコンストラクター内に初期化コードがあると想像してください。フィールドが新しいオブジェクトのアドレスで設定された後にこれらの命令が並べ替えられた場合、インスタンスは不完全です...クラスに次のコードがあると想像してください。

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;

それは違いを生みますか?あなたが単一のスレッドについて考えるならば、いいえ。複数のスレッドについて考える場合は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;

ハッピーコーディング!


1
CLRが初期化される前にオブジェクトへのアクセスを許可する場合、それはセキュリティホールです。パブリックコンストラクターのみが「SecureMode = 1」を設定し、インスタンスメソッドがそれをチェックする特権クラスを想像してみてください。コンストラクターを実行せずにこれらのインスタンスメソッドを呼び出すことができる場合、セキュリティモデルから抜け出し、サンドボックスに違反する可能性があります。
MichaelGG 2014年

1
@MichaelGG:あなたが説明した場合、そのクラスがそれにアクセスするために複数のスレッドをサポートするのであれば、それは問題です。コンストラクター呼び出しがジッターによってインライン化されている場合、CPUは、格納されている参照が完全に初期化されていないインスタンスを指すように命令を並べ替えることができます。これは回避可能であるため、CLRのセキュリティ問題ではありません。このようなクラスのコンストラクター内で、インターロック、メモリバリア、ロック、揮発性フィールドを使用するのはプログラマーの責任です。
ミゲルアンジェロ

2
ctor内のバリアはそれを修正しません。ctorが完了する前に、CLRが新しく割り当てられたオブジェクトへの参照を割り当て、メンバーを挿入しない場合、別のスレッドが半初期化されたオブジェクトに対してインスタンスメソッドを実行する可能性があります。
MichaelGG 2014年

これは、C#のDCLの場合にReSharper2016 / 2017が提案する「代替パターン」です。OTOH、Javaはないの結果があることを保証するnew。..完全に初期化される
user2864740

MS .netの実装により、コンストラクターの最後にメモリバリアが配置されることは知っていますが、申し訳ありませんが安全です。
ミゲルアンジェロ

7

実際に質問に答えた人はいないと思いますので、やってみます。

揮発性と最初のif (instance == null)ものは「必要」ではありません。ロックにより、このコードはスレッドセーフになります。

したがって、問題は、なぜ最初のものを追加するのかということですif (instance == null)

その理由はおそらく、コードのロックされたセクションを不必要に実行することを避けるためです。ロック内のコードを実行している間、そのコードも実行しようとする他のスレッドはブロックされます。これにより、多くのスレッドからシングルトンに頻繁にアクセスしようとすると、プログラムの速度が低下します。言語/プラットフォームによっては、回避したいロック自体からのオーバーヘッドが発生する可能性もあります。

したがって、最初のnullチェックは、ロックが必要かどうかを確認するための非常に迅速な方法として追加されています。シングルトンを作成する必要がない場合は、ロックを完全に回避できます。

ただし、何らかの方法でロックせずに参照がnullであるかどうかを確認することはできません。これは、プロセッサキャッシュが原因で、別のスレッドが参照を変更し、「古い」値を読み取って、不必要にロックを入力する可能性があるためです。しかし、あなたはロックを避けようとしています!

したがって、ロックを使用せずに最新の値を確実に読み取るために、シングルトンを揮発性にします。

volatileは、変数への1回のアクセス中にのみ保護するため、内部ロックが必要です。ロックを使用せずに安全にテストアンドセットすることはできません。

さて、これは実際に便利ですか?

まあ、私は「ほとんどの場合、いいえ」と言うでしょう。

Singleton.Instanceがロックのために非効率を引​​き起こす可能性がある場合、なぜこれが重大な問題になるほど頻繁に呼び出すのですか?シングルトンの要点は、1つしかないため、コードでシングルトン参照を1回読み取ってキャッシュできることです。

このキャッシュが不可能であると私が考えることができる唯一のケースは、スレッドが多数ある場合です(たとえば、新しいスレッドを使用してすべてのリクエストを処理するサーバーは、それぞれが数百万の非常に短期間のスレッドを作成する可能性がありますSingleton.Instanceを1回呼び出す必要があります)。

したがって、ダブルチェックロックは、パフォーマンスが非常に重要な特定のケースで実際に使用されるメカニズムであると思われます。その後、実際に何を実行するのか、それが実行されるのかを考えずに、誰もが「これが適切な方法です」というバンドワゴンをよじ登っています。彼らがそれを使用している場合、実際に必要になります。


6
これは、間違っていることと要点を見逃していることの間のどこかにあります。volatileダブルチェックロックのロックセマンティクスとは何の関係もありません。メモリモデルとキャッシュコヒーレンスと関係があります。その目的は、あるスレッドが別のスレッドによってまだ初期化されている値を受け取らないようにすることです。これは、ダブルチェックロックパターンが本質的に妨げないものです。Javaでは間違いなくvolatileキーワードが必要です。.NETでは、ECMAによると間違っているが、ランタイムによると正しいため、あいまいです。いずれにせよ、lock間違いなくそれを世話しませ
アーロノート2011年

え?あなたの発言が私が言ったことと一致しないところがわかりません。また、揮発性がロックのセマンティクスに何らかの形で関連しているとは言いませんでした。
ジェイソンウィリアムズ

6
あなたの答えは、このスレッドの他のいくつかのステートメントのようにlock、コードがスレッドセーフになると主張しています。その部分は真実ですが、ダブルチェックロックパターンはそれを危険にする可能性があります。それはあなたが欠けているように見えるものです。この答えは、の理由であるスレッドセーフの問題に対処することなく、ダブルチェックロックの意味と目的について蛇行しているようですvolatile
アーロノート2011年

1
instanceでマークされている場合、どうすれば安全ではなくなりvolatileますか?
userControl 2013年

5

ダブルチェックロックパターンでは、volatileを使用する必要があります。

ほとんどの人は、この記事を揮発性が必要ない証拠として指摘しています:https//msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10

しかし、最後まで読むことはできません。「警告の最後の言葉-既存のプロセッサで観察された動作からx86メモリモデルを推測しているだけです。したがって、ハードウェアとコンパイラは時間の経過とともにより積極的になる可能性があるため、ローロック技術も脆弱です。 。この脆弱性がコードに与える影響を最小限に抑えるためのいくつかの戦略を次に示します。まず、可能な限り、ローロック手法を避けます。(...)最後に、暗黙の保証に依存する代わりに、揮発性の宣言を使用して、可能な限り最も弱いメモリモデルを想定します。 。」

さらに説得力が必要な場合は、ECMA仕様に関するこの記事を読んでください。他のプラットフォームでも使用されます:msdn.microsoft.com/en-us/magazine/jj863136.aspx

さらに説得力が必要な場合は、揮発性なしで機能しないように最適化が行われる可能性があるというこの新しい記事を読んでください:msdn.microsoft.com/en-us/magazine/jj883956.aspx

要約すると、現時点ではvolatileがなくても機能する可能性がありますが、適切なコードを記述し、volatileまたはvolatileread / writeメソッドを使用する可能性はありません。そうしないことを提案する記事では、コードに影響を与える可能性のあるJIT /コンパイラの最適化のリスクの一部と、コードを壊す可能性のある将来の最適化が省略されている場合があります。また、前回の記事で述べたように、揮発性なしで動作するという以前の仮定は、ARMではすでに成り立たない可能性があります。


1
いい答えです。この質問に対する唯一の正しい答えは、単純な「いいえ」です。これによると、受け入れられた答えは間違っています。
デニスカッセル

3

AFAIK(そして-これは注意してください、私は多くの並行作業を行っていません)いいえ。ロックは、複数の候補(スレッド)間の同期を提供するだけです。

一方、volatileは、キャッシュされた(そして間違った)値に遭遇しないように、毎回値を再評価するようにマシンに指示します。

http://msdn.microsoft.com/en-us/library/ms998558.aspxを参照し、次の引用に注意してください

また、インスタンス変数にアクセスする前にインスタンス変数への割り当てが完了するように、変数は揮発性であると宣言されています。

揮発性の説明:http//msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx


2
「ロック」は、揮発性と同じ(またはそれ以上の)メモリバリアも提供します。
ヘンクホルターマン

2

探していたものが見つかったと思います。詳細はこの記事にあります-http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10

要約すると、この状況では、.NETでは揮発性修飾子は実際には必要ありません。ただし、より弱いメモリモデルでは、遅延開始オブジェクトのコンストラクターで行われた書き込みは、フィールドへの書き込み後に遅延する可能性があるため、他のスレッドは最初のifステートメントで破損した非nullインスタンスを読み取る可能性があります。


1
その記事の一番下、特に著者が述べている最後の文を注意深く読んでください。「警告の最後の言葉-私は既存のプロセッサで観察された動作からx86メモリモデルを推測しているだけです。したがって、ローロック技術も壊れやすいです。ハードウェアとコンパイラは時間の経過とともにより積極的になる可能性があります。この脆弱性がコードに与える影響を最小限に抑えるためのいくつかの戦略を次に示します。まず、可能な限り、ローロック手法を避けます。(...)最後に、可能な限り最も弱いメモリモデルを想定します。暗黙の保証に頼るのではなく、揮発性の宣言を使用する。」
user2685937 2017年

1
さらに説得力が必要な場合は、ECMA仕様に関するこの記事を読んでください。他のプラットフォームでも使用されます:msdn.microsoft.com/en-us/magazine/jj863136.aspxさらに説得力が必要な場合は、最適化が行われる可能性があるというこの新しい記事を読んでください。揮発性なしで動作するのを妨げる:msdn.microsoft.com/en-us/magazine/jj883956.aspx要約すると、現時点では揮発性なしで動作する可能性がありますが、適切なコードを記述して使用することはできません。 volatileまたはvolatileread / writeメソッド。
user2685937 2017年

1

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) {
          ...
        }
    }
}

Jon Skeetは、彼の記事(yoda.arachsys.com/csharp/singleton.html)で、この場合の適切なメモリバリアには揮発性が必要であると述べています。マーク、これについてコメントできますか?
コンスタンチン

ああ、私はダブルチェックされたロックに気づいていませんでした。単純に:そうしないでください; -p
MarcGravell

実際、ダブルチェックロックはパフォーマンス的には良いことだと思います。また、ロック内でアクセスされているときにフィールドを揮発性にする必要がある場合、ダブルチェックロックは他のどのロックよりも悪くはありません...
Konstantin

しかし、それはジョンが言及する別のクラスのアプローチと同じくらい良いですか?
MarcGravell

-3

これは、ダブルチェックロックでのvolatileの使用に関するかなり良い投稿です。

http://tech.puredanger.com/2007/06/15/double-checked-locking/

Javaでは、変数を保護することが目的である場合、変数が揮発性としてマークされていればロックする必要はありません。


3
興味深いですが、必ずしも非常に役立つとは限りません。JVMのメモリモデルとCLRのメモリモデルは同じものではありません。
bcat 2009
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.