Паттерн 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-у
Используйте для многопоточных случаев именно этот вариант. Он прост, не делает ничего слабодокументированного, его трудно сломать и он легко переносится на любой язык, где есть атомарные операции.