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

79

У меня есть несколько вопросов относительно шаблона 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;
      }
   }
}

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

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

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

         return instance;
      }
   }

Дорогое ли выполнение блокировки?

Уэйн Фиппс
источник
17
Кстати, у Джона Скита есть блестящая статья о безопасности потоков в Singletons: csharpindepth.com/Articles/General/Singleton.aspx
Arran
ленивая статическая инициализация была бы предпочтительнее ...
Митч Уит,
1
У меня также есть другие примеры с пояснениями здесь: csharpindepth.com/Articles/General/Singleton.aspx
Серж Волошенко
Точно такой же вопрос здесь для мира Java.
RBT

Ответы:

133

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

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

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

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

Джон
источник
1
Спасибо за информативный ответ с полезными ссылками. Очень признателен.
Уэйн Фиппс,
Перепроверенная блокировка - ссылка больше не работает.
El Mac
Простите, я имел в виду другой.
El Mac
1
@ElMac: Веб-сайт Скита отключен от банкомата, он будет восстановлен в свое время. Я запомню это и прослежу, чтобы ссылка работала, когда она появится, спасибо.
Джон
3
Начиная с .NET 4.0 Lazy<T>, эта работа отлично справляется.
ilyabreev 01
34

Lazy<T>Версия:

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

    public static Singleton Instance
        => lazy.Value;

    private Singleton() { }
}

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

Andasa
источник
Я получаю «System.MissingMemberException: 'Ленивый инициализированный тип не имеет общедоступного конструктора без параметров». С этим кодом в .Net 4.6.1 / C # 6.
ttugates
@ttugates, вы правы, спасибо. Код обновлен с помощью обратного вызова фабрики значений для ленивого объекта.
andasa
14

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

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

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

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

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

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

Джон Ханна
источник
1
Мне очень нравится, что ты думаешь здесь. Это отличный способ посмотреть на это. Хотел бы я принять два ответа или +5 к этому, большое спасибо
Уэйн Фиппс
2
Одно из следствий, которое становится важным, когда приходит время посмотреть на производительность, - это разница между разделяемыми структурами, которые могут быть задействованы одновременно, и теми, которые будут . Иногда мы не ожидаем, что такое поведение будет происходить часто, но это может быть, поэтому нам нужно заблокировать (требуется всего одна ошибка блокировки, чтобы все испортить). В других случаях мы знаем, что множество потоков действительно одновременно обращаются к одним и тем же объектам. В других случаях мы не ожидали, что будет много параллелизма, но ошибались. Когда вам нужно повысить производительность, приоритет отдается тем, у кого много параллелизма.
Джон Ханна,
По вашему мнению, volatileэто не обязательно, но должно быть readonly. См. Stackoverflow.com/q/12159698/428724 .
wezten
7

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

Heinzi
источник
4

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

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

Кевин Госсе
источник
3

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



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

Евгений Чернявский
источник
Не делает ли переменную экземпляра изменчивой, делает то же самое?
Ε Г И І И О
1

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

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

    private Singleton() { }

    public static Singleton Instance()
    {
        return instance;
    }
}
Брайан Огден
источник
0

Это называется механизмом блокировки с двойной проверкой, сначала мы проверим, создан ли экземпляр. Если нет, то только мы синхронизируем метод и создадим экземпляр. Это резко повысит производительность приложения. Выполнение блокировки тяжело. Поэтому, чтобы избежать блокировки, нам нужно сначала проверить нулевое значение. Это также потокобезопасность и лучший способ добиться максимальной производительности. Обратите внимание на следующий код.

public sealed class Singleton
{
    private static readonly object Instancelock = new object();
    private Singleton()
    {
    }
    private static Singleton instance = null;

    public static Singleton GetInstance
    {
        get
        {
            if (instance == null)
            {
                lock (Instancelock)
                {
                    if (instance == null)
                    {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
}
Путь пранаи
источник
0

Другая версия Singleton, в которой следующая строка кода создает экземпляр Singleton во время запуска приложения.

private static readonly Singleton singleInstance = new Singleton();

Здесь CLR (Common Language Runtime) позаботится об инициализации объекта и безопасности потоков. Это означает, что нам не потребуется явно писать какой-либо код для обеспечения безопасности потоков в многопоточной среде.

«Стремительная загрузка в шаблоне проектирования синглтона - это не процесс, в котором нам нужно инициализировать объект синглтона во время запуска приложения, а не по запросу, и держать его готовым в памяти для использования в будущем».

public sealed class Singleton
    {
        private static int counter = 0;
        private Singleton()
        {
            counter++;
            Console.WriteLine("Counter Value " + counter.ToString());
        }
        private static readonly Singleton singleInstance = new Singleton(); 

        public static Singleton GetInstance
        {
            get
            {
                return singleInstance;
            }
        }
        public void PrintDetails(string message)
        {
            Console.WriteLine(message);
        }
    }

из основного:

static void Main(string[] args)
        {
            Parallel.Invoke(
                () => PrintTeacherDetails(),
                () => PrintStudentdetails()
                );
            Console.ReadLine();
        }
        private static void PrintTeacherDetails()
        {
            Singleton fromTeacher = Singleton.GetInstance;
            fromTeacher.PrintDetails("From Teacher");
        }
        private static void PrintStudentdetails()
        {
            Singleton fromStudent = Singleton.GetInstance;
            fromStudent.PrintDetails("From Student");
        }
Джайдип Шил
источник
Хорошая альтернатива, но не отвечает на вопрос, который
Уэйн Фиппс
не напрямую, но может использоваться в качестве альтернативы «Thread Safe C # Singleton Pattern».
Jaydeep Шил
0

Отражательный узор Singleton:

public sealed class Singleton
{
    public static Singleton Instance => _lazy.Value;
    private static Lazy<Singleton, Func<int>> _lazy { get; }

    static Singleton()
    {
        var i = 0;
        _lazy = new Lazy<Singleton, Func<int>>(() =>
        {
            i++;
            return new Singleton();
        }, () => i);
    }

    private Singleton()
    {
        if (_lazy.Metadata() == 0 || _lazy.IsValueCreated)
            throw new Exception("Singleton creation exception");
    }

    public void Run()
    {
        Console.WriteLine("Singleton called");
    }
}
Роберт
источник