88

複数のテキストでは、.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)」が必要なメモリの一貫性を達成しないのはなぜですか? 「ロック」ステートメントの後、読み取りと書き込みの両方が揮発性になり、必要な一貫性が達成されるというのは本当ではありませんか?

4

8 に答える 8

62

揮発性は不要です。まあ、ちょっと**

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

ジョセフ・アルバハリは、このことを私がこれまで以上にうまく説明しています。

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


update :
*volatile変数の読み取りがVolatileReads になり、書き込みが s になりますVolatileWrite。これは、x86 および CLR の x64 ではMemoryBarrier. 他のシステムでは、より細かく設定されている場合があります。

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

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

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


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


マルチスレッドに関するluis abreu のシリーズでも、概念の概要がわかりやすく説明されて
います。 com/blogs/luisabreu/archive/2009/07/03/multithreading-introducing-memory-fences.aspx

于 2009-12-27T01:06:29.760 に答える
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;

違いはありますか?単一のスレッドを考える場合はNOです。はい、複数のスレッドを考えている場合...次の直後にスレッドが中断された場合はどうなりますか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;

ハッピーコーディング!

于 2012-10-18T00:38:31.617 に答える
7

誰も実際に質問に答えていないと思うので、試してみます。

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

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

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

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

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

したがって、シングルトンを揮発性にして、ロックを使用する必要なく、最新の値を確実に読み取れるようにします。

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

さて、これは実際に役に立ちますか?

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

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

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

したがって、ダブルチェックロックは、非常に特定のパフォーマンスが重要な場合に実際の場所を持つメカニズムであると思われます。彼らがそれを使用している場合、実際に必要になります。

于 2009-12-27T08:55:52.747 に答える
3

私の知る限り(そして-これは注意してください、私は多くの同時処理を行っていません)いいえ。ロックは、複数の競合者 (スレッド) 間の同期を提供するだけです。

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

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

また、インスタンス変数にアクセスできるようになる前にインスタンス変数への代入が完了するように、変数は volatile として宣言されます。

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

于 2009-12-27T00:20:50.510 に答える
2

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

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

于 2009-12-27T22:56:13.440 に答える
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) {
          ...
        }
    }
}
于 2009-12-27T09:19:22.230 に答える
-3

これは、二重チェックされたロックで volatile を使用することに関する非常に良い投稿です。

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

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

于 2009-12-27T00:19:21.947 に答える