Потокобезопасный Шаблон C# Singleton


у меня есть некоторые вопросы относительно шаблона синглтона, как описано здесь: http://msdn.microsoft.com/en-us/library/ff650316.aspx

следующий код является выдержкой из статьи:

using System;

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

в частности, в приведенном выше примере есть ли необходимость сравнивать экземпляр с null дважды, до и после блокировки? Это необходимо? Почему бы не выполнить блокировку сначала и сделать сравнение?

есть ли проблема в упрощении следующее?

   public static Singleton Instance
   {
      get 
      {
        lock (syncRoot) 
        {
           if (instance == null) 
              instance = new Singleton();
        }

         return instance;
      }
   }

является ли выполнение блокировки дорогим?

7 60

7 ответов:

выполнение блокировки жутко дорого по сравнению с простой проверкой указателя instance != null.

рисунок, который вы видите здесь, называется двойная проверка блокировки. Его цель состоит в том, чтобы избежать дорогостоящей операции блокировки, которая будет необходима только один раз (когда синглтон впервые доступен). Реализация такова, потому что она также должна гарантировать, что при инициализации синглтона не будет ошибок, возникающих в результате гонки потоков условия.

подумайте об этом так: голой null проверить (без lock) гарантированно даст вам правильный полезный ответ только тогда, когда что ответ "да, объект уже построен". Но если ответ "еще не построен", то у вас недостаточно информации, потому что вы действительно хотели знать, что он " еще не построен и никакой другой поток не собирается строить его в ближайшее время". Таким образом, вы используете внешнюю проверку в качестве очень быстрого начального теста и вы инициируете правильную, без ошибок, но" дорогую "процедуру (блокировка, а затем проверка), только если ответ"нет".

выше реализации достаточно хорошо для большинства случаев, но на данный момент это хорошая идея, чтобы пойти и прочитать статья Джона Скита о синглетах в C#, который также оценивает другие варианты.

ленивый вариант:

public sealed class Singleton
{
    static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton());
    private Singleton() { }

    public static Singleton Instance => lazy.Value;
}

требуется .NET 4 и C# 6.0 (VS2015) или новее.

выполнение блокировки: довольно дешево (все еще дороже, чем нулевой тест).

выполнение блокировки, когда она есть у другого потока: вы получаете стоимость всего, что им еще нужно сделать во время блокировки, добавленную к вашему собственному времени.

выполнение блокировки, когда другой поток имеет его, и десятки других потоков также ждут его: калечение.

по соображениям производительности вы всегда хотите иметь блокировки, которые хочет другой поток, на самый короткий период времени вообще вероятный.

конечно, легче рассуждать о" широких " замках, чем о узких, поэтому стоит начать с них широко и оптимизировать по мере необходимости, но есть некоторые случаи, которые мы узнаем из опыта и знакомства, где более узкий соответствует шаблону.

(кстати, если вы можете просто использовать private static volatile Singleton instance = new Singleton() или если вы можете просто не использовать синглтоны, а использовать статический класс вместо этого, оба лучше в отношении этих проблем).

причина в производительности. Если instance != null (что всегда будет иметь место, за исключением самого первого раза), нет необходимости делать дорогостоящий lock: два потока, обращающиеся к инициализированному синглтону одновременно, будут синхронизированы без необходимости.

почти в каждом случае (то есть: всех случаях, кроме самых первых), instance не будет null. Приобретение блокировки является более дорогостоящим, чем простая проверка, поэтому проверка один раз значение instance перед блокировкой хорошая и бесплатная оптимизация.

этот шаблон называется двойной проверкой блокировки:http://en.wikipedia.org/wiki/Double-checked_locking

Джеффри Рихтер рекомендует следующее:



    public sealed class Singleton
    {
        private static readonly Object s_lock = new Object();
        private static Singleton instance = null;

        private Singleton()
        {
        }

        public static Singleton Instance
        {
            get
            {
                if(instance != null) return instance;
                Monitor.Enter(s_lock);
                Singleton temp = new Singleton();
                Interlocked.Exchange(ref instance, temp);
                Monitor.Exit(s_lock);
                return instance;
            }
        }
    }

вы можете охотно создать потокобезопасный одноэлементный экземпляр, в зависимости от потребностей вашего приложения, это краткий код, хотя я бы предпочел ленивую версию @andasa.

public sealed class Singleton
{
    private static readonly Singleton instance = new Singleton();

    private Singleton() { }

    public static Singleton Instance()
    {
        return instance;
    }
}