Три возраста паттерна Singleton

Паттерн Singleton появился, пожалуй, как только появились статичные объекты. В Smalltalk-80 так был сделан ChangeSet, а чуть в самых разных библиотеках стали появляться сессии, статусы и тому подобные объекты, которых объединяло одно — они должны были быть одни-единственные на всю программу.

В 1994 году вышла известная книга «Паттерны проектирования», представив публике, среди 22-х прочих, и нашего героя, которого теперь назвали Singleton. Была там и его реализация на C++, вот такая:


//.h
class Singleton
{
public:
   static Singleton* Instance();
protected:
   Singleton();
private:
   static Singleton* _instance;
}
//.cpp
Singleton* Singleton::_instance = 0;
Singleton* Singleton::Instance() {
  if(_instance == 0){
     _instance = new Singleton;
  }
  return _instance;
}

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

Ничего удивительного — на дворе был 1995 год и многозадачные операционные системы были слишком медленными, чтобы кого-то смутить.

В любом случае, этот код не стареет. Используйте подобную реализацию всегда, если класс, который вы хотите объявить, заведомо не будет вызываться из нескольких потоков.

В 1995 году Скотт Майерс выпускает свою вторую книгу о хитростях C++. Среди прочего, он призывает в ней использовать Singleton вместо статичных классов — чтобы экономить память и точно знать, когда выполнится его конструктор.

Именно в этой книге появился каноничный синглтон Майерса и я не вижу причин, чтобы не привести его здесь:
class singleton
{
public:
   static singleton* instance() {
      static singleton inst;
      return &inst;
   }
private:
  singleton() {}
};


Аккуратно, лаконично и умело обыгран стандарт языка. Локальная статичная переменная в функции будет вызвана тогда и только тогда, когда будет вызвана сама функция.

Потом его расширили, запретив чуть больше операций:

class CMySingleton
{
public:
  static CMySingleton& Instance()
  {
    static CMySingleton singleton;
    return singleton;
  }

// Other non-static member functions
private:
  CMySingleton() {}                                  // Private constructor
  ~CMySingleton() {}
  CMySingleton(const CMySingleton&);                 // Prevent copy-construction
  CMySingleton& operator=(const CMySingleton&);      // Prevent assignment
};

Согласно новому стандарту C++11, больше для поддержки потоков ничего и не надо. Но до полной его поддержки всеми компиляторами надо ещё дожить.

А пока вот уже не меньше полутора десятков лет лучшие умы пытались поймать многопоточный singleton в клетку языкового синтаксиса. C++ не поддерживал потоки без сторонних библиотек — так что очень скоро почти под каждую библиотеку с потоками появился свой Singleton, который был «лучше всех прочих». Александреску уделяет им целую главу, отечественные разработчики борются с ним не на жизнь, а на смерть, а некто Андрей Насонов тоже долго экспериментирует и в итоге предлагает… совершенно другое решение.

В 2004 Мейерс и Александреску объединили усилия и описали Singleton с Double-check locking. Идея проста — если синглтон не обнаружен в первом if-е, делаем lock, и уже внутри проверяем ещё раз.

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

Для начала — тот самый вариант с double-check locking. Microsoft советует записывать его вот так:
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;
      }
   }
}


Скит, однако, считает, что этот код плох. Почему?

— Это не работает в Java. Модель памяти Java до версии 1.5 не проверяла, завершилось ли выполнение конструктора прежде, чем присвоить значение. К счастью, это уже не актуально — давно вышла Java 1.7, а Microsoft рекомендует этот код и гарантирует, что он будет работать.
— Его легко поломать. Запутаешься в скобках — и всё.
— Из-за lock-а он достаточно медлителен
— Есть лучше

Были и варианты без использования потоковых интерфейсков.

В частности, известная реализация через readonly поле. По мнению Скита (и Microsoft), это первый заслуживающий внимания вариант: Вот как он выглядит:

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

    // Explicit static constructor to tell C# compiler
    // not to mark type as beforefieldinit
    static Singleton()
    {
    }

    private Singleton()
    {
    }

    public static Singleton Instance
    {
        get
        {
            return instance;
        }
    }
}



Этот вариант тоже thread-safe и основан на любопытном свойстве полей readonly — они иницализируются не сразу, а при первом вызове. Замечательная идея, и сам автор рекомендует использовать именно её.

Есть ли у этой реализации недостатки? Разумеется, да:

— Если у класса есть статичные методы, то при их вызове readonly поле инициализируется автоматически.
— Конструктор может быть только статичным. Это особенность компилятора — если конструткор не статичен, то тип будет помечен как beforefieldinit и readonly создадутся одновременно со static-ами.
— Статичные конструкторы нескольких связанных Singleton-ов могут нечаянно зациклить друг друга, и тогда уже ничто не поможет и никто не спасёт.

Наконец, известнейшая lazy-реализация с nested-классом.
public sealed class Singleton
{
    private Singleton()
    {
    }

    public static Singleton Instance { get { return Nested.instance; } }

    private class Nested
    {
        // Explicit static constructor to tell C# compiler
        // not to mark type as beforefieldinit
        static Nested()
        {
        }

        internal static readonly Singleton instance = new Singleton();
    }
}


Недостатки у него те же самые, что у любого другого кода, который использует nested-классы.

В последних версиях C# появился класс System.Lazy, который всё это инкупсулирует. А значит, реализация стала ещё короче:
public sealed class Singleton
{
    private static readonly Lazy<Singleton> lazy =
        new Lazy<Singleton>(() => new Singleton());

    public static Singleton Instance { get { return lazy.Value; } }

    private Singleton()
    {
    }
}


Легко заметить, что и реализации с readonly, и вариант с nested-классом, и его упрощение в виде lazy объекта не работают с потоками. Вместо этого они используют сами структуры языка, которые «обманывают» интерпретатор. В этом их важнейшее отличие от double-lock'а, который работает именно с потоками.

Почему нехорошо «обманывать» язык? Потому что каждый такой «хак» очень легко нечаянно поломать. И потому что от него нет никакой пользы людям, которые пишут на других языках — а ведь паттерн предполагает универсальность.

Лично я считаю, что проблему потоков надо решать стандартными средствами. В C# встроено множество классов и целые ключевые слова для работы с многопоточностью. Почему бы не использовать стандартные средства, вместо того, чтобы пытаться «обмануть» компилятор.

Как я уже сказал, lock — не лучшее решение. Дело в том, что компилятор разворачивает вот такой lock(obj):

lock(this) {
   // other code
}


примерно в такой код:

Boolean lockTaken = false;
try {
   Monitor.Enter(this, ref lockTaken);
   // other code
}
finally {
  if(lockTaken) Monitor.Exit(this);
}


Джеффри Рихтер считает этот код весьма неудачным. Во-первых, try — это очень медленно. Во-вторых, если try рухнул, то в коде что-то не то. И когда второй поток начнёт его выполнять, то ошибка скорее всего повторится. Поэтому он призывает использовать для обычных потоков Monitor.Enter / Monitor.Exit, а Singleton переписать на атомарных операциях. Вот так:

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


Временная переменная нужна, потому что стандарт C# требует от компилятора сначала создавать переменную, а потом его присваивать. В итоге может получиться так, что в instance уже не null, но инициализация singleton-а ещё не завершена. См. описание подобный случаев в 29-й главе CLR via C# Джеффри Рихтера, раздел The Famous Double-Check Locking Technique.

Таким образом, нашлось место и double-lock-у

Используйте для многопоточных случаев именно этот вариант. Он прост, не делает ничего слабодокументированного, его трудно сломать и он легко переносится на любой язык, где есть атомарные операции.
Поделиться публикацией
Ой, у вас баннер убежал!

Ну, и что?
Реклама
Комментарии 61
  • +21
    На последний вариант — все же если несколько потоков запросят Instance — создадутся несколько копий синглтона. В ссылку instance запишется только одна из них конечно, и все потоки получат только одну, но тем не менее. Если это например, логгер, который сразу должен открыть файл, или коннект к базе данных, то такой вариант неприемлем. Или если класс «тяжелый», что его создание и ининциализация «дороже», чем lock, то использование интерлока просто невыгодно.

    Скажем так — это приемлемо если должен «жить» только один объект, а если же должен быть «создан» только один объект, то нет.
    Вариант с lock() как раз таки гарантирует создание только одного объекта, что под синглтон больше подходит и по совместимости/переносимости в частности и по логике вообще.
    • +14
      И вообще — имхо, «экономия» на операциии, которая происходит один раз есть овчинка не стоящая выделки.
      • 0
        Я тоже не вижу особых проблем с double lock-ом — всё-таки все его недостатки уже успели устареть. Хотя и с Рихтером согласен — try-catch в этом случае не нужен.

        Другое дело, что варианты с readonly и nested-классом расползлись по уйме книг и теперь много кто уверен, что именно «хаками» такое и надо писать.
    • 0
      Недопонял по поводу последнего кусочка кода. Как он позволяет обработать ситуацию, при которых потоки, находясь на инструкции
       if(instance != null) return instance; 
      считывают null и переходят к инструкции
      Singleton temp = new Singleton();

      ?
      • +1
        Всё вы правильно поняли :). Поток создаёт temp, а потом _атомарно_ переносит его через Interlocked.CompareExchange. В итоге следующий поток уже не может ничего записать — Interlocked же.

        Последний вариант я взял из CLR via C# by Jeffry Richter, последняя, 29-я глава. Там довольно подробно объясняется, почему так лучше.
        • 0
          Хитро, конечно, придумано, возьму на заметку. Хотя, мне кажется, что идиома синглетона рушится таким подходом — ведь когда видят синглетон, справедливо полагают, что он должен конструироваться один раз. А тут вот так вычурно — не всем такой способ подойдет. Да и код с первого взгляда не понятен — кажется, что в нем допущена ошибка, и руки тянутся вписать блокировку :)
          • 0
            Ага, есть у него такая особенность :).

            Моё скромное IMHO — отлично реализованы и double-lock и Рихтеров. Именно потому, что если в языке/библиотеках к нему есть потоки, то должны быть и Lock, и атомарный перенос. А значит — будет не проблема перенести эти реализации куда-нибудь на Java.
      • +2
        Вообще, я лично остановился на double checked locking синглетонах. Если уж мне нужно сделать синглетон, и если он должен быть потокобезопасным — я как правило выбираю либо этот способ, либо просто статическое поле, инициализируемое в статическом же конструкторе (это если мне вообще без разницы, когда будет создан синглетон — ленивость не нужна). Все остальные способы либо страдают чем-то, либо зависят от деталей того, как рантайм или компилятор обрабатывают всякие тонкие моменты типа порядка инициализации статических переменных, beforefieldinit итд, либо чересчур сложны для решаемой задачи (черт возьми, ведь мне всего-навсего нужен долбанный синглетон! мне не хочется использовать для этого нюансы конкретно используемого языка). Про это вы как раз очень в точку отметили:
        Почему нехорошо «обманывать» язык? Потому что каждый такой «хак» очень легко нечаянно поломать. И потому что от него нет никакой пользы людям, которые пишут на других языках — а ведь паттерн предполагает универсальность.

        С производительностью у DCL тоже все в порядке — он проигрывает самым оптимально написанным синглетонам сущие копейки. Поэтому мой выбор — DCL, пусть он и не самый «идеальный» вариант с точки зрения перфекционистов.
        • +1
          А как думаете, не лучше ли вместо Lock писать нативный Monitor.Enter? Я тут больше ко мнению Jeffry Richter'а склоняюсь — сгенерированный комплятором try здесь а) тормозит б) если отвалится — уже не поможет.
          • 0
            На мой взгляд, внутри double-checked-lock'а (раз уж поток зашел внутрь) уже нет смысла экономить на спичках, поскольку этот код выполняется 1 раз за всю жизнь приложения, ну может быть 2 раза у самых-самых везучих.
            • 0
              А если конструктор обвалится? Например, зашёл один поток — обвалил конструктор, заходит второй поток — и снова обваливает конструктор, делая ещё хуже.
              • +2
                А как хуже-то? Повторно будет писаться в лог о том, что произошла ошибка? Или повторный try-finally приведет к деградации производительности? По сравнению с тем фактом, что *синглетон не удалось инстанцировать из-за исключения в конструкторе*, проблемы такого рода представляются мне малозначимыми :)
                • +3
                  Ну, это-то верно. Поздно пить боржоми, когда конструктор отвалился.
                • 0
                  А что будет в случае интерлоков? Предполагая, что конструкторы везде одинаковы и различие лишь в синглтоновом методе Instance — обвалятся точно так же. Только параллельно в разных тредах, а не последовательно в случае лока.
                  Даже я бы сказал — будет ровно наоборот, если речь идет об открытии файла на запись, например, то в случае лока все пройдет нормально, а в случае интерлока получим одно нормальное выполнение и (n-1) исключений в других потоках, сигнализирующих о невозможности открыть файл. И эти ситуации еще нужно обработать как то, чтобы эти потоки получили нормальный instance, а не null.
          • +3
            Насколько public static class Singleton, при использовании C#, хуже всего вышеописанного?
            • +4
              Не намного. Но есть свои особенности. Например:

              — static класс невозможно от чего-то унаследовать — а значит, не получится собрать абстрактную фабрику (фабрики рекомендуют объявлять синглтонами ещё в самой первой книжке).
              — когда выполнится static-конструктор — никто не знает. Вроде бы «где-то в начале». К тому же, для ASP.Net-приложений может быть актуальным совмещение фабрика по производству синглтонов :) — по синглтону на сессию. Ведь в ASP.Net статичные переменный глобальны для всего приложения.
              — Когда падает конструктор статического класса — всё падает. Когда падает конструктор синглтона — падает только функция, которая неосторожно его вызвала
              — Невозможность засунуть в статичный класс счётчик состояний (например, чтобы можно было откатить его к предыдущим настройкам).
            • 0
              Не намного. Но есть свои особенности. Например:

              — static класс невозможно от чего-то унаследовать — а значит, не получится собрать абстрактную фабрику (фабрики рекомендуют объявлять синглтонами ещё в самой первой книжке).
              — когда выполнится static-конструктор — никто не знает. Вроде бы «где-то в начале». К тому же, для ASP.Net-приложений может быть актуальным совмещение фабрика по производству синглтонов :) — по синглтону на сессию. Ведь в ASP.Net статичные переменный глобальны для всего приложения.
              — Когда падает конструктор статического класса — всё падает. Когда падает конструктор синглтона — падает только функция, которая неосторожно его вызвала
              — Невозможность засунуть в статичный класс счётчик состояний (например, чтобы можно было откатить его к предыдущим настройкам).
              • –1
                Вывод один — надо знать свой язык, тогда любой паттерн можно реализовать грамотно и красиво!
                • +5
                  я нисколько не знаю C++, но именно такие статьи, разбирающие глубокие технические аспекты технических тем, вызывающие уйму комментариев с разбором полётов на эту же тему, делают хабр «тем». искреннее вам спасибо! лучи плюсов вам в карму и профессиональных успехов!
                  • +2
                    Я и сам удивлён. Оказывается, мои рабочие выписки и компиляции интересны и в наше облачное время :)
                  • 0
                    > Почему нехорошо «обманывать» язык? Потому что каждый такой «хак» очень легко нечаянно поломать.

                    Как можно нечаянно поломать вариант c Lazy?

                    > И потому что от него нет никакой пользы людям, которые пишут на других языках — а ведь паттерн
                    > предполагает универсальность.

                    Паттерн это прежде всего сама идея. А оптимизировать код для переноса между языками надо только если стоит такая задача.
                    • +2
                      Странно. Приводите эволюцию паттерна в С/С# и забыли про еще одного гиганта, где этот шаблон изменялся не менее интересно. Вот вариант, который рекомендует Джошуа Блох (так же основан на стандарте языка — Java Language Specification):

                      public enum Singleton {
                          INSTANCE;
                      }
                      

                      Я писал об этом тут.
                      • НЛО прилетело и опубликовало эту надпись здесь
                        • 0
                          Спасибо, что подбросили ссылку. Дело в том, что при форматировании статьи я нечаянно грохнул все ссылки на внешние ресурсы (включая и вот эту статью).
                        • НЛО прилетело и опубликовало эту надпись здесь
                          • +2
                            При чем здесь Apple?
                            • –2
                              Он вместо «патерна» прочитал «патента» и типа пошла ассоциация.
                              • НЛО прилетело и опубликовало эту надпись здесь
                            • +2
                              Не стоит также забывать, что в наше время Синглтон уже считается антипаттерном (Wikipedia, 2й параграф)
                              • +1
                                Ага, и практически в каждом приложении используется IoС контейнер, который сам разруливает «синглетонность».

                                Меня удивляет постоянное всплывание темы потокобезопасных синглтонов, соглашусь в одним из предыдущих ораторов с тем, что написание и возможные проблемы от ошибок в синхронизации стоят больше нежели лишнее время на немедленное инициализирование при старте приложения.
                                • 0
                                  Вики, конечно, редкостный авторитет в этих вопросах. Не говоря уже о том, что там написано «некоторые считают», а не «считается».

                                  Ну и да, аргументируйте своими словами, почему синглтон как паттерн (а не предложенные реализации!) — это антипаттерн.
                              • 0
                                Для себя лично пришёл к выводу, что паттерн синглтон перешёл черту между полезный/проблемный в сторону «проблемный»… А по поводу статьи, если честно, не понял почему даблчек с локом тормозной вариант? Синглтон на и то синглтон, что он создаёт инстанс крайне редко если вообще не один раз на всё приложение. И лок, соответственно, будет вызван только один раз (ну два-три раза если звезды потоки сойдутся вместе), когда инстанс создастся. В чём же тормоз для приложения???? Ежли в приложении 100+ синглтонов, которые могут тормозить, то это уже проблема архитектуры, как мне кажется.
                                • +1
                                  Статья родилась из рабочих заметок, так что многое просто не пояснил.

                                  Он тормозной потому, что два try/finally делает. Поэтому мне кажется, что лучше напрямую писать Monitor.Enter/Exit — будет то же самое, но без лишних обработчиков.

                                  Ну и сам факт того, что представляет из себя lock интересен. А то находятся люди, которые его ещё раз try-ем оборачивают.
                                  • 0
                                    Видимо вы не уловили суть моего вопроса. :)

                                    Он тормозной потому, что два try/finally делает.

                                    Мой основной язык хоть и си++, но сути это не меняет, пусть там lock или обычный mutex — не суть дело, каждый по своему привносит с собой не одну сотню лишних тактов. Мой вопрос в том, разве нельзя принебречь этим? По сути lock или обработка mutex'а происходят только тогда, когда надо создать первый инстанс, а затем они более не вызываются т.к. первое условие более не выполняется:

                                    if (instance == null) {
                                        lock (syncRoot) {
                                           if (instance == null)
                                              instance = new Singleton();
                                       }
                                    }
                                    

                                    Поэтому мне не совсем ясны причины беспокойства о тормознутости. Где она проявляется?
                                    • 0
                                      В принципе, в случае синглтона она нигде не проявляется.

                                      Это имеет отношение, разумеется, не к singletone, а к lock-у самому по себе.
                                • +1
                                  ОК, я был слишком краток ссылаясь на Википедию. На самом деле там есть ссылки на статьи которые объясняют почему это считается антипеттерном. К слову, я специально написал «считается» вместо «является».

                                  Во времена написания «GoF» синглтон был актуальным шаблоном. В настоящее время подходы к разработке изменились. Модульные тесты являются практически стандартной инженерной практикой. При написании тестов Синглтоны оказываются проблемой. Заменить mock'ом без модификации его практически не возможно.

                                  Вторая неприятность с Синглтоном — он делает зависимости неочевидными. Глядя на интерфейс класса, невозможно узнать увидеть все его зависимости. Для этого нужно смотреть реализацию медодов.

                                  Проблема, которую он решает Синглтон — это убедиться, что в системе существует один, и только один экземпляр ресурса. В подовляющем большинстве случаев эту проблему можно решить другим способом — используя Dependency Injection.

                                  Синглтон имеет право на существование, и нужно занать как его правильно реализовать. Но каждый раз, когда вы собираетесь его использовать, вы должны доказать себе и тому, кто будет мейнтейнить Ваш код, что в этой ситуации Синглтон действительно необходим.
                                  • +2
                                    Ваш комментарий — это наглядный пример путаницы между собственно паттерном и его реализациями.

                                    Сначала давайте посмотрим, что такое синглтон. Из вики: «the singleton pattern is a design pattern that restricts the instantiation of a class to one object». Из Каннингема: «Ensure a class has only one instance, and provide a global point of access to it.» GoF под рукой, к сожалению, нет, но насколько я знаю, тамошнее определение у Каннингема и процитировано.

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

                                    А вот теперь рассмотрим ваши примеры.

                                    Модульные тесты являются практически стандартной инженерной практикой. При написании тестов Синглтоны оказываются проблемой. Заменить mock'ом без модификации его практически не возможно.

                                    Что вы хотите протестировать? Очевидно, не сам синглтон, потому что при его тестировании никаких проблем не возникет. Вероятно, вы хотите протестировать класс, зависящий от синглтона. Но эта постановка задачи неверна — класс должен зависеть от абстракции, а не от конкретной реализации (иначе бы никакие замены были невозможно, и мы бы не говорили о модульном тестировании). Следовательно, то, что вы тестируете, должно рассчитывать на получение экземпляра определенной абстракции (интерфейса, класса — не так важно), и, в принципе, даже не обязано знать о том, сколько этих экземпляров в системе. Следующий вопрос — откуда этот экземпляр взять, и вот он к синглтону как паттерну отношения не имеет.

                                    Вторая неприятность с Синглтоном — он делает зависимости неочевидными. Глядя на интерфейс класса, невозможно узнать увидеть все его зависимости. Для этого нужно смотреть реализацию медодов.

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

                                    Проблема, которую он решает Синглтон — это убедиться, что в системе существует один, и только один экземпляр ресурса. В подовляющем большинстве случаев эту проблему можно решить другим способом — используя Dependency Injection.

                                    Дело в том, что вы путаете две вещи.

                                    Смотрите, два примера выше уже должны были натолкнуть на мысль, что с точки зрения кода-пользователя синглтон — это просто зависимость. Как любая зависимость, он должен быть абстрагирован (IoC). Соответственно, после этого все пользователи должны рассматривать эту функциональную единицу как еще одну зависимость, работать с которой нужно точно так же, как и со всеми другими. При этом эта зависимость может быть синглтоном (и все пользователи получат один экземпляр), а может им не быть (и тогда все получат свой).

                                    Разделяйте dependency management и instance/construction/lifetime management. Мы можем взять DI как парадигму и Unity как IoC-контейнер, реализовать DI через вбрасывание в конструктор, а для конкретной зависимости указать, что ее Lifetime manager — ContainerControlled. Получим синглтон в DI — потому что у класса есть ровно один экземпляр. Можем сделать все то же самое, но выбрать не DI, а ServiceLocation (например, потому, что у нас нет контроля за созданием объектов) — получим синглтон через локатор (визуально практически не отличающийся от обычного синглтона).

                                    Повторю еще раз логическую цепочку:
                                    • для модульного тестирования нужен IoC (он нужен не только для этого, но это приведенный вами пример)
                                    • с точки зрения IoC синглтонов не существует, потому что синглтон — это реализация некоей функциональности, а потребители в IoC оперируют абстракциями, а не реализациями
                                    • для того, чтобы внедрить IoC, вам нужно управление зависимостями (явное, с помощью DI, или неявное, с помощью service location, фабрик и так далее)
                                    • каждая зависимость имеет разный жизненный цикл. Синглтон является одним из вариантов жизненного цикла зависимости


                                    Общий вывод: синглтон как паттерн создания объектов не устарел (и антипаттерном не является). Устарели его реализации, большую часть которых сейчас можно рассматривать как узкозаточенный сервис-локатор со всеми недостатками сервис-локаторов.
                                    • 0
                                      Аргументрованные споры я люблю :)

                                      Про реализацию синглтона я не сказал ни слова, автор топика все отлично описал. Я сказал что этот паттерн в принципе решает одну проблему, но при этом добавляет еще две.

                                      Вы приводите аргументы, подтверждающие проблемность Синглтона, но делаете противополжный вывод :)

                                      Вероятно, вы хотите протестировать класс, зависящий от синглтона. Но эта постановка задачи неверна — класс должен зависеть от абстракции, а не от конкретной реализации (иначе бы никакие замены были невозможно, и мы бы не говорили о модульном тестировании). Следовательно, то, что вы тестируете, должно рассчитывать на получение экземпляра определенной абстракции (интерфейса, класса — не так
                                      важно), и, в принципе, даже не обязано знать о том, сколько этих экземпляров в системе.
                                      Абсолютно верно.
                                      Следующий вопрос — откуда этот экземпляр взять, и вот он к синглтону как паттерну отношения не имеет.

                                      Вот где Синглтон создает проблему.
                                      Допустим у меня есть какой-то ресурс к которому нужен монопольный доступ. Синглтон предлагает изолировать его и предоставить один метод для доступа к ресурсу, скажем MyResource.getInstance().
                                      Теперь у меня есть класс, для которого я хочу написать юнит-тест, но он использует Синглтон. В юнит тесте я не хочу создавать реальный экземпляр ресурса. Я хочу заменить его моком. Как мне это сделать? В книге «Working effectively with legacy code» предлагается сделать лазейку в виде сеттера MyResource.setInstance(). Но тогда есть опасность что этот метод будет использован кроме тестов еще и в продакшн коде.

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

                                      Абсолютно верно.

                                      Вместо в место Синглтонов и статических фабрик предпочтительнее использовать DI. Это позволит избежать описанной выше проблемы с тестированием.

                                      Разделяйте dependency management и instance/construction/lifetime management. Мы можем взять DI как парадигму и Unity как IoC-контейнер, реализовать DI через вбрасывание в конструктор, а для конкретной зависимости указать, что ее Lifetime manager — ContainerControlled. Получим синглтон в DI — потому что у класса есть ровно один экземпляр. Можем сделать все то же самое, но выбрать не DI, а ServiceLocation (например, потому, что у нас нет контроля за созданием объектов) — получим синглтон через локатор (визуально практически не отличающийся от обычного синглтона).
                                      Абсолютно верно.

                                      Нужно разделять стадию Construction и стадию выполнения. На стадии выполния следует использовать те зависимости, которые были предоставлены DI контейнером, а не использовать Синглтон.
                                      На стадии конструирования у нас есть полный контроль на тем сколько экземпляров мы создали, и в использовании Синглтона нет необходимости.

                                      Общий вывод: На стадии конструирования Синглтон не нужен, а на стадии выполния его следует избегать.
                                      • 0
                                        Синглтон предлагает изолировать его и предоставить один метод для доступа к ресурсу, скажем MyResource.getInstance().

                                        Нет. Синглтон предлагает предоставить глобальный доступ. А вариант, который предлагаете вы (статический метод класса) — это всего лишь вариант реализации глобального доступа. С равным успехом вы можете сделать ServiceLocator.Resolve(typeof(MyResource)) или .ctor(Func{of MyResource} resourceGetter).

                                        В книге «Working effectively with legacy code» предлагается сделать лазейку в виде сеттера MyResource.setInstance(). Но тогда есть опасность что этот метод будет использован кроме тестов еще и в продакшн коде.

                                        Типовой решение — не собирать этот метод для продакшн-кода (или статический анализ, или отдельная сборка и internal-access — решений много).

                                        Вместо в место Синглтонов и статических фабрик предпочтительнее использовать DI.

                                        Вы, кажется, не совсем поняли, что я сказал. Правильной формулировкой было бы: «вместо service location и фабрик предпочтительно использовать DI», и это утверждение, скажем так, не совсем верно, потому что у явных зависимостей есть свои недостатки.

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

                                        А вот это как раз неверно. На стадии конструирования нам надо определить, сколько экземпляров мы создали, и если для какого-то класса мы создаем один экземпляр, то это и есть паттерн Singleton.
                                        • 0
                                          Кажется у нас тут с Вами путаница в терминологии.
                                          Похоже, Вы под Сингтоном подразумеваете единственный ЭКЗЕМПЛЯР ресурса.
                                          Я же имею в виду СПОСОБ это обесепечить.
                                          • 0
                                            Именно поэтому я и начал с описания паттерна.

                                            В описании сказано: «гарантируйте, что у класса один экземпляр». Все остальное — это реализации паттерна, которые могут быть разными.
                                            • +1
                                              Вот она причина нашего недопонимания и как следствие спор о том, что чуше теплое или мягкое.

                                              Каждый паттерн состоит из нескольких частей:
                                              — Проблема, которую он решает
                                              — Способ решить проблему архитектурно (На уровне ролей)

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

                                              А детали реализации зависят от языка и необходимости в ленивой инициализации или в поддержке многопоточности. Именно детали описаны в топике.

                                              • 0
                                                Способ решения — «Сделайте класс Синглтон, который который контроллирует создание экземпляра и предоставляет доступ к этому экземпляра»

                                                У нас с вами разный способ решения. Я свой специально цитировал в самом начале беседы: «Ensure a class has only one instance, and provide a global point of access to it.»
                                                • +1
                                                  У Вас ошибочное пониманимание того что является Паттерном.

                                                  «Ensure a class only has one instance, and provide a global point of access to it.» — это всего лишь секция «Intent» в описании патерна.

                                                  Цитата из GoF:
                                                  In general, a pattern has four essential elements:
                                                  1. The pattern name is a handle we can use to describe a design problem, its solutions, and consequences in a word or two.....
                                                  2. The problem describes when to apply the pattern......
                                                  3. The solution describes the elements that make up the design, their relationships, responsibilities, and collaborations. The solution doesn't describe a particular concrete design or implementation, because a pattern is like a template that can be applied in many different situations. Instead, the pattern provides an abstract description of a design problem and how a general arrangement of elements (classes and objects in our case) solves it.
                                                  4. The consequences are the results and trade-offs of applying the pattern.....



                                                  Из определения видно, что описание необходимых классов входит паттерн.

                                                  Согласно GoF, для реализации паттерна Singlton нужен всего один класс:
                                                  Participants
                                                  • Singleton
                                                    • defines an Instance operation that lets clients access its unique instance. Instance is a class operation (that is, a class method in Smalltalk and a static member function in C++).
                                                    • may be responsible for creating its own unique instance.




                                                  Тепрь цитирую Вас:
                                                  А вот это как раз неверно. На стадии конструирования нам надо определить, сколько экземпляров мы создали, и если для какого-то класса мы создаем один экземпляр, то это и есть паттерн Singleton.


                                                  Единственный экземпляр класса в системе не является Синглтоном.
                                                  Если я создаю объект и через DI передаю его объектам — это не синглтон.
                                                  Синглтон — это паттерн, состоящий из вспомогательного класса, который предоставляет доступ к тому единственному экземпляру.
                                                  • 0
                                                    Я уже сказал, что опираюсь не на GoF.

                                                    Ну и да, обратите внимание на may в «may be responsible for creating its own unique instance». Что означает, что вы можете внутрь операции Instance спрятать что угодно, включая DI, и это все равно будет синглтон.
                                  • +1
                                    Тем, кто считает, что синглтон double-checked locking работает всегда и везде, рекомендую прочитать статью Рихтера/Александреску и убедиться в обратном

                                    www.nwcpp.org/Downloads/2004/DCLP_notes.pdf
                                    • 0
                                      Ура! Наконец-то и эту запостили!

                                      Респект и уважуха!
                                      • 0
                                        Я не умею читать, или в этой статье нет ни слова про C#?
                                        • 0
                                          Кстати про C# — в статье упоминалось это habrahabr.ru/post/130318/ первоисточник по ссылкам искать лень.
                                          Тип перестановки Перестановка разрешена
                                          Загрузка-загрузка Да
                                          Загрузка-запись Да
                                          Запись-загрузка Да
                                          Запись-запись Нет

                                          То есть проблема перестановки, указанная в этой статье, в С# не имеет места.
                                          Операции записи в поля класса конструктором не могут поменяться местами с записью в ссылочное поле instance.
                                          • 0
                                            Дополнил статью с учётом критики (теперь там и double-lock, разумеется). Заодно ещё раз перерыл все материалы и уточнил, почему атомарная операция нужна даже если поток залочен.
                                      • 0
                                        Кстати, я полез и перечитал раздел Рихтера, на который вы ссылаетесь (29-я глава, раздел The Famous Double-Check Locking Technique).

                                        Так вот, ваш пример
                                        if(instance != null) return instance;
                                        Monitor.Enter(s_lock);
                                        Singleton temp = new Singleton();
                                        Interlocked.CompareExchange(ref instance, temp, null);
                                        Monitor.Exit(s_lock);
                                        return instance;
                                        

                                        — это смесь из двух примеров Рихтера.

                                        Первый выглядит вот так:
                                        // If the Singleton was already created, just return it (this is fast)
                                        if (s_value != null) return s_value;
                                        Monitor.Enter(s_lock); // Not created, let 1 thread create it
                                        if (s_value == null) {
                                            // Still not created, create it
                                            Singleton temp = new Singleton();
                                            // Save the reference in s_value (see discussion for details)
                                            Interlocked.Exchange(ref s_value, temp);
                                        }
                                        Monitor.Exit(s_lock);
                                        // Return a reference to the one Singleton object
                                        return s_value;
                                        


                                        Обратите внимание на Monitor.* и Interlocked.Exchange.

                                        А вот второй:
                                        if (s_value != null) return s_value;
                                        // Create a new Singleton and root it if another thread didn't do it first
                                        Singleton temp = new Singleton();
                                        Interlocked.CompareExchange(ref s_value, temp, null);
                                        // If this thread lost, then the second Singleton object gets GC'd
                                        return s_value; // Return reference to the single object
                                        


                                        Обратите внимание отсутствие Monitor.* и Interlocked.CompareExchange.

                                        А в вашем примере — одновременно и блокировка, и CompareExchange. Какой-то франкенштейн, вы не находите?

                                        Ну и да, о варианте с CompareExchange Рихтер пишет, что его можно применять только тогда, когда конструктор не имеет побочных эффектов.

                                        (ну и да, полезно понимать, что первая реализация, которая с Monitor.*, блокирует доступ навсегда, и реальный ее посыл в том, чтобы если нам не удалось создать синглтон, то приложение надо ронять полностью)
                                        • 0
                                          Спасибо, подправил :). Действительно, когда сверял код, то скопировал не ту строку.

                                          Такими темпами и с таким обсуждением, чую, скоро у нас здесь ещё лучшая реализация появится.
                                          • 0
                                            А зачем? «Классическая» реализация Singleton уже давно представляет исключительно академический интерес, в практике она не нужна.
                                        • +1
                                          double-checked singleton не работает на архитектурах, поддерживающих out-of-order execution. На этих архитектурах его можно реализовать, только с использованием memory barriers.

                                          www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
                                          • 0
                                            А где-нибудь есть информация, какие именно это архитектуры? На x86, насколько я понимаю, этой проблемы нет.
                                            • 0
                                              Из наиболее распространенных архитектур точно знаю, что out-of-order execution поддерживается sparc v8/v9, IA64, powerpc.
                                              Про х86-64 верно только про AMD, так как Intel IA64 тоже поддерживает барьеры.
                                              Когда нужно было разобраться, я смотрел исходный код gcc и opensolaris.
                                            • 0
                                              А семантика volatile в .NET отличается от таковой, которая появилась в JMM с версии 1.5? Если да, то чем, можете пояснить? А если нет, то почему тогда не работает? Начиная с Java 5 проблемы более не существует.
                                            • 0
                                              Про С#/.NET не знаю. Под «не работает» — я имел в виду не работает double-checked singleton в каноническом виде, приведенном выше.
                                              • 0
                                                csharpindepth.com/articles/general/singleton.aspx
                                                Совпадение? Не думаю.
                                                А где пометка «перевод»?
                                                • 0
                                                  Эта заметка упомянута в статье.

                                                  Статья не о том, какие бывают. а о том, какой откуда пошёл. Например, в примерах даже от MS обычно пишут double-lock, не указывая. кто и зачем его придумал.

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

                                                Самое читаемое