在.Net中我需要进行双重检查锁定

时间:2013-02-26 23:07:35

标签: .net multithreading locking

如果我想懒惰实例化一个包含大量数据的类,并且我希望它只有一个实例(Singleton),那么在设置实例之前是否需要仔细检查对象? (。净)。或者只能在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;
    }
}

}

3 个答案:

答案 0 :(得分:2)

从不需要双重检查锁定 - 它纯粹是一种性能优化,它依赖于依赖于平台的内存排序技巧来避免大多数情况下的锁定。单一检查锁定总是足够,更便携,更简单,所以你应该更喜欢它,除非基准测试显示你真的需要这么小的提升。但是有一个更好的解决方案。

你应该完全避免明确锁定单身人士。相反,尝试静态初始化器,因为它们很简单,(线程)安全,快速,并且可以延迟加载。

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

此类实例不是在应用程序启动时创建的,但在首次使用类型或字段之前有些懒惰。确切的规则是depend on whether a static constructor is present,但是如果初始化程序有时会在必要时执行,那么这很好。在.NET v4上,初始化为very lazy。 99%的时间这应该是您的首选实施,因为它是最快,最简单的实现。即使在.NET v3.5及更早版本中,也只有在遇到引用类型的方法时才会加载。

此代码可以比基于锁的版本更快,因为一旦类完全加载,访问该字段就不需要保护和锁定。特别是,JIT可以简单地假设变量已经设置,理论上甚至可以省略诸如空值检查和循环中的提升读取之类的事情。如果你确实需要精确控制延迟加载的时间;尝试更简单的锁定然后检查而不是双重检查锁定(它依赖于一些棘手的内存模型细节) - 但在实践中,我怀疑几乎没有人需要精确控制;你只是想避免不必要的工作。

关于双重检查锁定:据我所知,即使在.NET上,你需要volatile关键字进行双重检查锁定才能完全可移植:.NET {{3}的ARM实现你已经习惯了x86。即使它适用于ARM和各种单声道平台,为什么使用这样一个复杂的实现,如果它比一个简单的静态初始化器慢?


基准测试结果

  AlwaysLock init
  37.09 nanoseconds per iteration (1000000 iters of AlwaysLock)
  DoubleCheckedLocking init
  2.78 nanoseconds per iteration (1000000 iters of DoubleCheckedLocking)
  StaticInitializer init
  2.13 nanoseconds per iteration (1000000 iters of StaticInitializer)
  StaticConstructor  init
  2.56 nanoseconds per iteration (1000000 iters of StaticConstructor)

  38.45 nanoseconds per iteration (10000000 iters of AlwaysLock)
  2.07 nanoseconds per iteration (10000000 iters of DoubleCheckedLocking)
  1.57 nanoseconds per iteration (10000000 iters of StaticInitializer)
  1.57 nanoseconds per iteration (10000000 iters of StaticConstructor)

  21.71 nanoseconds per iteration (10000000 sync iters of AlwaysLock)
  4.62 nanoseconds per iteration (10000000 sync iters of DoubleCheckedLocking)
  3.15 nanoseconds per iteration (10000000 sync iters of StaticInitializer)
  3.17 nanoseconds per iteration (10000000 sync iters of StaticConstructor)

基准代码

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

不要对单例使用锁定,请使用静态初始化器。

答案 1 :(得分:1)

单次检查就可以了。

双重检查仅出于性能原因。由于锁只是为了防止多个线程同时创建实例,所以只有在您真正需要创建实例时才需要它。

答案 2 :(得分:0)

这是我的答案: 正如@Eamon所建议的,解决对静态对象的线程访问的最佳方法是使用静态初始化器初始化静态对象(在这种情况下,您不需要锁来访问实例)(另外,检查Eamon的代码如何使用静态ctor可以使得在第一次访问类之前不分配实例。

但是,如果你不能使用静态初始化器,我认为使用双重检查锁定是有意义的,因为它可以提高性能。从我的测试中,我发现仅当我调用Instance方法超过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 performance results