2011-08-26 11 views
10

私は最近the following post on the Resharper websiteに出くわしました。それは、ダブルチェックロックの議論で、次のコードを持っていた:私たちはinitが()intializeするために使用される方法であると仮定した場合ダブルチェックロックのメモリモデル保証

public class Foo 
{ 
    private static volatile Foo instance; 
    private static readonly object padlock = new object(); 

    public static Foo GetValue() 
    { 
     if (instance == null) 
     { 
      lock (padlock) 
      { 
       if (instance == null) 
       { 
        instance = new Foo(); 
        instance.Init(); 
       } 
      } 
     } 
     return instance; 
    } 

    private void Init() 
    { 
     ... 
    } 
} 

ポストは、その

主張を作ります Fooの場合、上記のコードは、 メモリモデルが読み書きの順序を保証しないため、期待どおりに機能しない可能性があります。 結果として、変数 のインスタンスが一貫性のある状態になる前に、Init()の呼び出しが実際に発生することがあります。

ここでは私の質問です:

  1. (少なくとも2.0以降).NETメモリモデルがlockが提供するであろうからinstanceは、volatileとして宣言する必要がないを持っていることを私の理解でしたフルメモリフェンス。それは事実ではないのですか?または私は間違っていましたか?

  2. 複数のスレッドに関してのみ読み書き可能な再順序付けはありませんか? 1つのスレッドで副作用が一貫した順序になり、lockが他のスレッドが何かを見逃してしまうのを防ぐことができたと私は理解していました。私はここでもオフベースですか?

+2

.NET 2.0メモリモデルについては正しいです。あなたは 'volatile'を必要としません。なぜなら、' lock'は実際に完全なフェンスを行います。しかし、Chibacityが指摘しているように、スレッドセーフについて言えば、競合状態を見落とすことは非常に簡単です。 – Steven

答えて

18

例との大きな問題は、インスタンスがnullではないかもしれないので、最初のnullチェックがロックされていないということですが、前に初期化が呼び出されました。これは、Initが呼び出される前にインスタンスを使用するスレッドにつながる可能性があります。

正しいバージョンは、したがって、次のようになります。

public static Foo GetValue() 
{ 
    if (instance == null) 
    { 
     lock (padlock) 
     { 
      if (instance == null) 
      { 
       var foo = new Foo(); 
       foo.Init(); 
       instance = foo; 
      } 
     } 
    } 

    return instance; 
} 
+2

それは非常に鋭いです。私は自分自身を逃した。完全性のためにあなたの答えにそのコードの正しいバージョンを追加していませんか? – Steven

+2

@スチーブン補正。編集のための乾杯 - 感謝します。私の電話から非常に難しい! :) –

+3

私はこの携帯電話に答えるために余分なポイントを得るべきです:-) – Steven

1

私が正しくコードを読めば、問題は次のとおりです。

発信者1は、メソッドを開始します真であることがヌル==インスタンスを見つけ、入りますロックし、STILLがnullであるインスタンスを見つけ、インスタンスを作成します。

Init()が呼び出される前に、呼び出し元1のスレッドは中断され、呼び出し元2はメソッドに入ります。呼び出し元2は、インスタンスがnullでないことを検出し、呼び出し元1が初期化できるようになる前にそのインスタンスを使用します。それは「完全なフェンス」を作成しますが、どのような引用が参照していることは「二​​重のケースをロック確認」の「そのフェンスの内側」に何が起こっている一方で

0

...説明http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx

を参照それは述べて:

However, we have to assume that a series of stores have taken place during construction 
of ‘a’. Those stores can be arbitrarily reordered, including the possibility of delaying 
them until after the publishing store which assigns the new object to ‘a’. At that point, 
there is a small window before the store.release implied by leaving the lock. Inside that 
window, other CPUs can navigate through the reference ‘a’ and see a partially constructed 
instance. 

あなたの例からinstanceと上記の文章でaを交換...

はさらに、この http://blogs.msdn.com/b/brada/archive/2004/05/12/130935.aspxをチェックアウト - それはあなたのシナリオで volatileが達成したことを説明します...

素敵なフェンスの説明とvolatileとどのようvolatileは、プロセッサに依存しても異なる効果を持っているあなたが見るhttp://www.albahari.com/threading/part4.aspx、さらに/よりよい情報それは.NET私の理解だったhttp://csharpindepth.com/Articles/General/Singleton.aspx

12

を参照してください上のコードを実行しますメモリモデル(少なくとも2.0以降) は、ロック が完全メモリフェンスを提供するので、そのインスタンスをvolatileとして宣言する必要はありません。そうでないか、私は 誤っていましたか?

これは必須です。理由は、lock以外のinstanceにアクセスしているためです。 volatileを省略し、すでにこのような初期化の問題を修正しているものとします。 C#コンパイラ、JITコンパイラ、またはハードウェアがtemp変数を離れて最適化し、Initが実行される前に割り当てられますしinstance変数の原因となる命令シーケンスを発することができ、いくつかのレベルでは

public class Foo 
{ 
    private static Foo instance; 
    private static readonly object padlock = new object(); 

    public static Foo GetValue() 
    { 
     if (instance == null) 
     { 
      lock (padlock) 
      { 
       if (instance == null) 
       { 
        var temp = new Foo(); 
        temp.Init(); 
        instance = temp; 
       } 
      } 
     } 
     return instance; 
    } 

    private void Init() { /* ... */ } 
} 

。実際には、コンストラクタが実行される前でもinstanceを割り当てることができます。 Initメソッドを使用すると、問題を見つけるのがはるかに簡単になりますが、問題はコンストラクタにも残ります。

これは、ロック内で命令を自由に並べ替えることができるため、有効な最適化です。 lockはメモリバリアを発行しますが、Monitor.EnterMonitor.Exitコールでのみ発生します。

ここで、volatileを省略すると、ほとんどのハードウェア実装とCLI実装の組み合わせでコードが機能するようになります。その理由は、x86ハードウェアのメモリモデルがより緊密で、MicrosoftのCLRの実装もかなり厳しいためです。ただし、この件に関するECMA仕様は比較的緩やかです。つまり、CLIの別の実装では、現在Microsoftが無視する最適化を自由に行うことができます。ほとんどの人が集中する傾向のあるハードウェアではなく、CLIジッタである弱いモデルをコーディングする必要があります。このため、まだvolatileが必要です。

複数のスレッドに関して読取り/書込み再順序付けは観測できません。単一のスレッドでは、側面 のエフェクトは一貫した順序であり、その場所のロック は、他のスレッドが何かを見逃してしまうのを防ぎます。 こちらもオフベースですか?

はい。命令の並べ替えは、複数のスレッドが同じメモリ位置にアクセスしている場合にのみ開始されます。たとえ最も弱いソフトウェアやハードウェアのメモリモデルであっても、コードがスレッド上で実行されているときに開発者が意図したものから動作を変更するような最適化は許可されません。それ以外の場合、プログラムは正しく実行されません。問題はどのように他のスレッドがそのスレッドで何が起こっているかを観察することです。他のスレッドは、実行中のスレッドの動作とは異なる動作を認識することがあります。しかし、実行スレッドは常に正しい動作を認識します。

いいえ、lockは、それだけで、他のスレッドが異なる一連のイベントを認識するのを妨げません。理由は、実行中のスレッドが、開発者が意図した順序とは異なる順序でlock内部の命令を実行している可能性があるからです。メモリバリアが作成されるのは、ロックの入口と出口だけです。したがって、あなたの例では、lockでこれらの命令をラップしたにもかかわらず、コンストラクタが実行される前でも新しいオブジェクトへの参照をinstanceに割り当てることができます。 volatileを使用して

は、他の一方で、lock振る舞う内部コードの共通の知恵にもかかわらず、法の冒頭にinstanceの初期チェックと比較してどのように大きな影響を与えています。多くの人々は、大きな問題はinstanceが揮発性の読み込みなしで失効している可能性があると考えています。そうかもしれないが、より大きな問題は、lock内に揮発性の書き込みがなければ、別のスレッドがコンストラクタがまだ実行されていないインスタンスを参照しているinstanceを参照する可能性があるということです。揮発性書き込みは、書き込み後にコンストラクタコードをinstanceに移動させないため、この問題を解決します。それがvolatileが依然として必要な大きな理由です。

+1

優れた答え。 –

関連する問題