2

大量のデータを保持するクラスを遅延インスタンス化し、インスタンスを1つだけにしたい場合(シングルトン)、インスタンスをセットアップする前にオブジェクトを再確認する必要がありますか?(。ネット)。または、AnotherWayForSingletonInstanceで行われるように、単一のチェックのみを実行しても問題ありません。

コードは次のとおりです。

class MyHeavyClass
{
private MyHeavyClass()
{
}

private static object _lock = new object();
private static MyHeavyClass _instance;
public static MyHeavyClass Instance{
    get{
        //check 1
        if (_instance == null)
        {
            lock(_lock)
            {
                //check 2
                if (_instance == null)
                {
                    _instance = new MyHeavyClass();
                }
            }
        }
        return _instance;
    }
}

public static MyHeavyClass AnotherWayForSingletonInstance{
    get{
        lock(_lock)
        {
            //check 1
            if (_instance == null)
            {
                _instance = new MyHeavyClass();
            }
        }
        return _instance;
    }
}

}

4

3 に答える 3

2

二重チェックのロックは必要ありませ。ほとんどの場合、ロックを回避するためにプラットフォームに依存するメモリオーダリングのトリックに依存する純粋なパフォーマンスの最適化です。シングルチェックロックは常に十分で、移植性が高く、単純であるため、ベンチマークでその小さなブーストが本当に必要であることが示されない限り、ロックを優先する必要があります。しかし、さらに良い解決策があります。

明示的にロックされたシングルトンは完全に避ける必要があります。代わりに、静的初期化子を試してください。静的初期化子は単純で、(スレッド)セーフで、高速で、遅延ロードされるためです。

sealed class MyHeavyClass
{
    MyHeavyClass() {}
    public static readonly MyHeavyClass Instance = new MyHeavyClass();
}

このようなインスタンスは、アプリケーションの起動時に作成されませんが、最初にタイプまたはフィールドが最初に使用される前に、やや遅れて作成されます。正確なルールは、静的コンストラクターが存在するかどうかによって異なりますが、初期化子が必要以上に早く実行されることがある場合は、これで問題ありません。.NET v4では、初期化は非常に怠惰です。99%の確率で、これは可能な限り最速で最も単純な実装であるため、頼りになる実装になるはずです。.NET v3.5以前でも、タイプを参照するメソッドが検出されるまで、これはロードされません。

クラスが完全にロードされると、フィールドにアクセスする際にガードやロックが不要になるため、このコードはロックベースのバージョンよりも高速になる可能性があります。特に、JITは変数が設定されていると単純に想定でき、理論的にはnullチェックやループからのホイスト読み取りなどを省略できます。遅延読み込みのタイミングを正確に制御する必要がある場合。ダブルチェックされたロック(いくつかのトリッキーなメモリモデルの詳細に依存します)ではなく、より単純なロックしてからチェックしてみてください。しかし、実際には、その正確な制御を必要とする人はほとんどいないと思います。不必要な作業を避けたいだけです。

ダブルチェックロックに関して:私が知る限り、.NETでも、volatileダブルチェックロックのキーワードを完全に移植可能にする必要があります。.NETのARM実装には、あなたと同じ書き込み順序の保証はありません。 x86で使用されました。ARMやさまざまなモノラルプラットフォームで動作する場合でも、単純な静的初期化子よりも遅いのに、なぜこのような複雑な実装を使用するのでしょうか。


ベンチマーク結果

  AlwaysLock init
  反復あたり37.09ナノ秒(AlwaysLockの1000000イター)
  DoubleCheckedLocking init
  反復あたり2.78ナノ秒(DoubleCheckedLockingの1000000反復)
  StaticInitializer init
  反復あたり2.13ナノ秒(StaticInitializerの1000000イター)
  StaticConstructor init
  反復あたり2.56ナノ秒(StaticConstructorの1000000イター)

  反復あたり38.45ナノ秒(AlwaysLockの10000000イター)
  反復あたり2.07ナノ秒(DoubleCheckedLockingの10000000反復)
  反復あたり1.57ナノ秒(StaticInitializerの10000000イター)
  反復あたり1.57ナノ秒(StaticConstructorの10000000反復)

  反復あたり21.71ナノ秒(AlwaysLockの10000000同期反復)
  反復あたり4.62ナノ秒(DoubleCheckedLockingの10000000同期反復)
  反復あたり3.15ナノ秒(StaticInitializerの10000000同期反復子)
  反復あたり3.17ナノ秒(StaticConstructorの10000000同期反復)

ベンチマークコード

void Main()
{
    const int loopSize = 10000000;

    Bench(loopSize/10, ()=> AlwaysLock.Inst);
    Bench(loopSize/10, ()=> DoubleCheckedLocking.Inst);
    Bench(loopSize/10, ()=> StaticInitializer.Inst);
    Bench(loopSize/10, ()=> StaticConstructor.Inst);
    Console.WriteLine();
    Bench(loopSize, ()=> AlwaysLock.Inst);
    Bench(loopSize, ()=> DoubleCheckedLocking.Inst);
    Bench(loopSize, ()=> StaticInitializer.Inst);
    Bench(loopSize, ()=> StaticConstructor.Inst);
    Console.WriteLine();
    SBench(loopSize, ()=> AlwaysLock.Inst);
    SBench(loopSize, ()=> DoubleCheckedLocking.Inst);
    SBench(loopSize, ()=> StaticInitializer.Inst);
    SBench(loopSize, ()=> StaticConstructor.Inst);

    //uncommenting the next lines will cause instantiation of 
    //StaticInitializer but not StaticConstructor right before this method.
    //var o = new object[]{ 
    //          StaticInitializer.Inst, StaticConstructor.Inst};
}

static void Bench<T>(int iter, Func<T> func) {
    string name = func().GetType().Name;
    var sw = Stopwatch.StartNew();
    Parallel.For(0,iter,i=>func());
    var sec = sw.Elapsed.TotalSeconds;
    Console.Write("{0:f2} nanoseconds per iteration ({1} iters of {2})\n"
        , sec*1000*1000*1000/iter, iter, name);
}

static void SBench<T>(int iter, Func<T> func) {
    string name = func().GetType().Name;
    var sw = Stopwatch.StartNew();
    for(int i=0;i<iter;i++) func();
    var sec = sw.Elapsed.TotalSeconds;
    Console.Write("{0:f2} nanoseconds per iteration ({1} sync iters of {2})\n"
        , sec*1000*1000*1000/iter, iter, name);
}

sealed class StaticInitializer {
    StaticInitializer(){ Console.WriteLine("StaticInitializer init"); }
    public static readonly StaticInitializer Inst = new StaticInitializer();
    //no static constructor, initialization happens before
    //the method  with the first access
}

sealed class StaticConstructor {
    StaticConstructor(){ Console.WriteLine("StaticConstructor  init"); }
    //a static constructor prevents initialization before the first access.
    static StaticConstructor(){}
    public static readonly StaticConstructor Inst = new StaticConstructor();
}

sealed class AlwaysLock {
    AlwaysLock(){ Console.WriteLine("AlwaysLock init"); }
    static readonly object _lock = new object();
    static AlwaysLock _instance;
    public static AlwaysLock Inst { get {
        lock(_lock)
            if (_instance == null)
                _instance = new AlwaysLock();
        return _instance;
    } }
} 


sealed class DoubleCheckedLocking {
    DoubleCheckedLocking(){ Console.WriteLine("DoubleCheckedLocking init"); }
    static readonly object _lock = new object();
    static DoubleCheckedLocking _instance;

    public static DoubleCheckedLocking Inst { get {
        if (_instance == null)
            lock(_lock)
                if (_instance == null)
                    _instance = new DoubleCheckedLocking();
        return _instance;
    } }
}

TL; DR

シングルトンにロックを使用せず、静的初期化子を使用します。

于 2013-02-26T23:15:37.237 に答える
1

1回のチェックで問題なく動作します。

ダブルチェックは、パフォーマンス上の理由でのみ実行されます。ロックは、複数のスレッドが同時にインスタンスを作成するのを防ぐためにのみ存在するため、実際にインスタンスを作成する必要がある場合にのみ必要です。

于 2013-02-26T23:26:43.727 に答える
0

これが私の答えです:@Eamonによって提案されたように、静的オブジェクトへのスレッドアクセスを解決する最良の方法は、静的初期化子を使用して静的オブジェクトを初期化することです(この場合、インスタンスにアクセスするためにロックは必要ありません)(また、チェックしてください静的ctorを使用して、クラスに最初にアクセスする前にインスタンスが割り当てられないようにする方法に関するEamonのコード)。

ただし、静的初期化子を使用できない場合は、パフォーマンスを向上させることができるため、ダブルチェックロックを使用するのが理にかなっていると思います。テストの結果、インスタンスメソッドを100,000回以上呼び出した場合にのみパフォーマンスが重要であることがわかりました。

私のコードとEamonのコードでは、十分な反復回数で、ダブルチェックロックを使用することでパフォーマンスが大幅に向上しました。

これが私のテストコードです(LinqPadで実行されます)

void Main()
    {
        Stopwatch sw = Stopwatch.StartNew();
        const int loopSize = 10000000;
        for (int i = 0; i < loopSize; i++)
        {
            SingletonSingleLock o = SingletonSingleLock.Instance;
        }

        sw.ElapsedMilliseconds.Dump();


        sw = Stopwatch.StartNew();
        for (int i = 0; i < loopSize; i++)
        {
            SingletonDoubleLock o = SingletonDoubleLock.Instance;
        }
        sw.ElapsedMilliseconds.Dump();
    }

    /* Test results
        Elapsed milliseconds
        # of calls to Instance      1,000       10,000      100,000     1,000,000       10,000,000
        SingletonSingleLock             0           0           4           39              433
        SingletonDoubleLock             0           0           1           17              185
    */

    public class SingletonSingleLock
    {
        private SingletonSingleLock()
        {

        }

        private static object _lock = new object();
        private static SingletonSingleLock _instance;
        public static SingletonSingleLock Instance
        {
            get
            {
                lock(_lock)
                {
                    if (_instance == null)
                    {
                        _instance = new SingletonSingleLock();
                    }
                }
                return _instance;
            }
        }

    }


    public class SingletonDoubleLock
    {
        private SingletonDoubleLock()
        {

        }

        private static object _lock = new object();
        private static SingletonDoubleLock _instance;

        public static SingletonDoubleLock Instance
        {
            get
            {
                if (_instance == null)
                {
                    lock(_lock)
                    {
                        if (_instance == null)
                        {
                            _instance = new SingletonDoubleLock();
                        }
                    }
                }
                return _instance;
            }
        }

    }

VSのプロファイラー結果のスクリーンショットに見られるように、ダブルチェックされたロックによってパフォーマンスが向上します。 VSパフォーマンス結果

于 2013-02-27T16:29:04.603 に答える