Паттерн Singleton появился, пожалуй, как только появились статичные объекты. В Smalltalk-80 так был сделан ChangeSet, а чуть в самых разных библиотеках стали появляться сессии, статусы и тому подобные объекты, которых объединяло одно — они должны были быть одни-единственные на всю программу.
В 1994 году вышла известная книга «Паттерны проектирования», представив публике, среди 22-х прочих, и нашего героя, которого теперь назвали Singleton. Была там и его реализация на C++, вот такая:
Что касается потоков, авторы про них даже и не пишут, считая эту проблему малоактуальной. Зато много внимания уделили всяким тонкостям наследования таких классов друг от друга.
Ничего удивительного — на дворе был 1995 год и многозадачные операционные системы были слишком медленными, чтобы кого-то смутить.
В любом случае, этот код не стареет. Используйте подобную реализацию всегда, если класс, который вы хотите объявить, заведомо не будет вызываться из нескольких потоков.
В 1995 году Скотт Майерс выпускает свою вторую книгу о хитростях C++. Среди прочего, он призывает в ней использовать Singleton вместо статичных классов — чтобы экономить память и точно знать, когда выполнится его конструктор.
Именно в этой книге появился каноничный синглтон Майерса и я не вижу причин, чтобы не привести его здесь:
Аккуратно, лаконично и умело обыгран стандарт языка. Локальная статичная переменная в функции будет вызвана тогда и только тогда, когда будет вызвана сама функция.
Потом его расширили, запретив чуть больше операций:
Согласно новому стандарту C++11, больше для поддержки потоков ничего и не надо. Но до полной его поддержки всеми компиляторами надо ещё дожить.
А пока вот уже не меньше полутора десятков лет лучшие умы пытались поймать многопоточный singleton в клетку языкового синтаксиса. C++ не поддерживал потоки без сторонних библиотек — так что очень скоро почти под каждую библиотеку с потоками появился свой Singleton, который был «лучше всех прочих». Александреску уделяет им целую главу, отечественные разработчики борются с ним не на жизнь, а на смерть, а некто Андрей Насонов тоже долго экспериментирует и в итоге предлагает… совершенно другое решение.
В 2004 Мейерс и Александреску объединили усилия и описали Singleton с Double-check locking. Идея проста — если синглтон не обнаружен в первом if-е, делаем lock, и уже внутри проверяем ещё раз.
А пока суд да дело, проблема потоково-безопасного Singleton переползла и на прочие C-подобные языки. Сперва — на Java, а затем и на C#. И вот уже Джон Скит предлагает целый набор решений, у каждого из которых есть и плюсы, и минусы. И их же предлагает Microsoft.
Для начала — тот самый вариант с double-check locking. Microsoft советует записывать его вот так:
Скит, однако, считает, что этот код плох. Почему?
— Это не работает в Java. Модель памяти Java до версии 1.5 не проверяла, завершилось ли выполнение конструктора прежде, чем присвоить значение. К счастью, это уже не актуально — давно вышла Java 1.7, а Microsoft рекомендует этот код и гарантирует, что он будет работать.
— Его легко поломать. Запутаешься в скобках — и всё.
— Из-за lock-а он достаточно медлителен
— Есть лучше
Были и варианты без использования потоковых интерфейсков.
В частности, известная реализация через readonly поле. По мнению Скита (и Microsoft), это первый заслуживающий внимания вариант: Вот как он выглядит:
Этот вариант тоже thread-safe и основан на любопытном свойстве полей readonly — они иницализируются не сразу, а при первом вызове. Замечательная идея, и сам автор рекомендует использовать именно её.
Есть ли у этой реализации недостатки? Разумеется, да:
— Если у класса есть статичные методы, то при их вызове readonly поле инициализируется автоматически.
— Конструктор может быть только статичным. Это особенность компилятора — если конструткор не статичен, то тип будет помечен как beforefieldinit и readonly создадутся одновременно со static-ами.
— Статичные конструкторы нескольких связанных Singleton-ов могут нечаянно зациклить друг друга, и тогда уже ничто не поможет и никто не спасёт.
Наконец, известнейшая lazy-реализация с nested-классом.
Недостатки у него те же самые, что у любого другого кода, который использует nested-классы.
В последних версиях C# появился класс System.Lazy, который всё это инкупсулирует. А значит, реализация стала ещё короче:
Легко заметить, что и реализации с readonly, и вариант с nested-классом, и его упрощение в виде lazy объекта не работают с потоками. Вместо этого они используют сами структуры языка, которые «обманывают» интерпретатор. В этом их важнейшее отличие от double-lock'а, который работает именно с потоками.
Почему нехорошо «обманывать» язык? Потому что каждый такой «хак» очень легко нечаянно поломать. И потому что от него нет никакой пользы людям, которые пишут на других языках — а ведь паттерн предполагает универсальность.
Лично я считаю, что проблему потоков надо решать стандартными средствами. В C# встроено множество классов и целые ключевые слова для работы с многопоточностью. Почему бы не использовать стандартные средства, вместо того, чтобы пытаться «обмануть» компилятор.
Как я уже сказал, lock — не лучшее решение. Дело в том, что компилятор разворачивает вот такой lock(obj):
примерно в такой код:
Джеффри Рихтер считает этот код весьма неудачным. Во-первых, try — это очень медленно. Во-вторых, если try рухнул, то в коде что-то не то. И когда второй поток начнёт его выполнять, то ошибка скорее всего повторится. Поэтому он призывает использовать для обычных потоков Monitor.Enter / Monitor.Exit, а Singleton переписать на атомарных операциях. Вот так:
Временная переменная нужна, потому что стандарт C# требует от компилятора сначала создавать переменную, а потом его присваивать. В итоге может получиться так, что в instance уже не null, но инициализация singleton-а ещё не завершена. См. описание подобный случаев в 29-й главе CLR via C# Джеффри Рихтера, раздел The Famous Double-Check Locking Technique.
Таким образом, нашлось место и double-lock-у
Используйте для многопоточных случаев именно этот вариант. Он прост, не делает ничего слабодокументированного, его трудно сломать и он легко переносится на любой язык, где есть атомарные операции.
В 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-у
Используйте для многопоточных случаев именно этот вариант. Он прост, не делает ничего слабодокументированного, его трудно сломать и он легко переносится на любой язык, где есть атомарные операции.
