Ещё раз о синглтоне

Настоящая статья написана под влиянием главы “Реализация шаблона синглтон” из широко известной книги А. Александреску[1] без особых претензий на оригинальность, хотя прилагаемый код может быть использован. Шаблон синглтон реализован в библиотеке Loki[2] (хотя и не вполне так, как описано в книге Александреску). Преимуществом предлагаемой реализации является её компактность и моё стремление использовать те новации стандартов языка С++, которые были приняты в последние годы, то есть после написания книги и библиотеки Loki.

Далее обсуждаются следующие вопросы:

1)      Что такое паттерн синглтон и какие требования он предъявляет к реализациям.
2)      Идиомы С++ для поддержки этого паттерна.
3)      Обеспечение уникальности синглтона, его корректное уничтожение и,  при необходимости, восстановление уничтоженного синглтона.
4)      Работа с синглтоном в многопоточной среде.
5)      Реализация  синглтона на основе шаблонного класса, параметризированного набором стратегий. 


Итак, синглтон “гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа”[3]. Александреску показывает, что попытка создать такой класс с использованием только статических членов и функций является неверной, на самом деле в этом случае мы имеем паттерн Monostate[4]. Наиболее приемлемым решением будет класс с единственной доступной (public) функцией, возвращающей указатель на единственный экземпляр класса.

class Singleton {
public:
static Singleton* Instance();
protected:
Singleton();
private:
static Singleton* _instance;
};

Singleton* Singleton::_instance = 0;

Singleton* Singleton::Instance () 
{
if (_instance == 0) {
_instance = new Singleton;
}
return _instance;
}

Конечно, в этом случае необходимо будет объявить закрытыми (private) конструктор класса, его конструктор копирования и операторы присваивания. Чтобы у пользователя не было соблазна вызвать оператор delete для указателя на экземпляр класса – придётся объявить закрытым и его деструктор.

Впрочем, С++11 предлагает альтернативный путь – пометить '= delete’ выше указанные функции и операторы. Пользователь в обоих вариантах получит ошибку компиляции при попытке их использования в клиентском коде.

Имеется важная проблема – как корректно уничтожить созданный экземпляр синглтона?
Конечно, как правило, синглтон существует до завершения программы и выделенная для него динамическая память с завершением процесса будет освобождена большинством современных операционных систем.
Но здесь есть важный момент – объект синглтона может обладать некоторыми ресурсами – объектами ядра, сокетами, файлами, ссылками на COM-объекты и проч. Просто бросить эти объекты при завершении программы некрасиво, да и может быть вредно. Поэтому важно при завершении программы правильно разрушить объект синглтона (вызвать его деструктор).

При использовании в программе нескольких синглтонов возникают ещё 2 проблемы.
  • Во-первых, при завершении программы может возникнуть необходимость использования уже корректно разрушенного синглтона (то есть восстановить уже разрушенный, использовать его и потом снова разрушить).
  • Во-вторых, в общем виде,  необходимо определить механизм последовательности их корректного разрушения при завершении программы (задать время жизни глобальных объектов и сиглтонов в частности).
Тему менеджера времени жизни синглтонов оставим пока (возможно она станет предметом отдельной статьи). Для интересующихся читателей предлагаю ознакомиться с работой [5].

Создание объекта синглтон в динамической памяти не всегда приемлемо. Альтернативный вариант предлагает Скотт Мейерс [6] – использовать локальную статическую переменную, конструктор которой вызывается при первом прохождении потока управления через её определение.

Singleton& Singleton::Instance()
{
static Singleton obj;
return obj;
}

Самым важным при этом является факт, что компилятор добавляет внутрь функции Instance() скрытый вызов функции
atexit(__DestroySingleton); [7],
в которую передаётся указатель на деструктор создаваемого объекта. На известных автору статьи компиляторах С++ этот механизм работает.

Таким образом, использующий статическую память класс может выглядеть так:
  
class Singleton {
public:
static Singleton* Instance();
private:
static Singleton* _instance;
};

Singleton* Singleton::_instance = 0;

Singleton* Singleton::Instance () {
if (_instance == 0) {
static Singleton obj;
_instance = &obj;
}
return _instance;
}

Но нам ничего не мешает вызывать функцию atexit() явно для регистрации функции удаления объекта в варианте создания его в динамической памяти.

Для решения проблемы восстановления разрушенного синглтона (феникс) возможно:
·         в варианте статической памяти : использование сочетания неявного вызова atexit() и явного оператора "placement new"[8] для воссоздания объекта на его “руинах”;
·         в варианте динамической памяти: вызовы atexit() и new для нового создания объекта.

При работе с функцией Instance() в многопоточной среде с целью предотвращения коллизий, связанных с состязанием за ресурсы предлагается блокировка с двойной проверкой[9].

Singleton& Singleton::Instance()
{
if (!pInstance_)
Guard myGuard(lock_); 
if (!pInstance_) 
{
pInstance_ = new Singleton;
}
}
return *pInstance_;
}

Стандарт C++11 решает проблему зависимости от платформы, введя в язык примитив синхронизации мьютекс[10].


Подводя итоги предыдущего обсуждения, в духе паттерна "Стратегия” мы должны выявить 3 типа стратегий для класса SingletonHolder:

1)      Стратегия создания (CreationPolicy) определяет: создаётся объект в динамической памяти оператором new (CreateUsingNew), выделением памяти функцией malloc() и вызовом конструктора по умолчанию(CreateUsingMalloc)  или создаётся в статической памяти(CreateStatic).
2)      Стратегия времени жизни (LifetimePolicy) определяет поведение объекта при завершении программы: разрушение объекта при завершении программы без возможности восстановления (DefaultLifetime). Попытка обращения по ссылке на разрушенный объект вызывает исключение std::logic_error. Альтернативой является стратегия (PhoenixLifetime), позволяющая при обращении по имеющимся у клиентов ссылкам воссоздавать уже разрушенные в ходе завершения программы объекты  и далее завершать их.
3)      Потоковая модель (ThreadingModel) определяет поведение функции Instance() в однопоточном (SingleThreaded) или многопоточном (MultiThreaded) окружении.

Реализацией является шаблонный класс-оболочка, шаблонными параметрами которого являются тип T, единственный экземпляр которого должен быть гарантирован и стратегии поведения этого экземпляра.


template
< 
            class T,
                        template <class> class CreationPolicy = CreateUsingNew,
                        template <class> class LifetimePolicy = DefaultLifetime,
                        template <class> class ThreadingModel = SingleThreaded
> 
class SingletonHolder;

Поскольку поведение класса-оболочки для различных сочетаний стратегий радикально отличается - в функции Instance() вызывается перегружаемая функция Work(), выбор которой при инстанцировании определяется по правилу SFINAE[11].
Одна функция Work() определена для сочетания стратегий CreateStatic и PhoenixLifetime и вторая для остальных возможных сочетаний.

Требования к типу T:
1)      Его конструктор по умолчанию и деструктор должны быть определены и объявлены закрытыми (private);
2)      Конструкторы копирования и операторы присваивания должны быть объявлены удаленными (delete);
3)      Классы стратегий создания и сам класс SingletonHolder должны быть объявлены друзьями типа T. Для ленивых достаточно добавить макрос AMIGOS в тело класса.

  
Необходимо учитывать, что экземпляры объектов класса T, созданные с помощью разных наборов стратегий, переданных в  SingletonHolder, будут разными (а не одним и тем же) экземплярами объектов класса T.


template< class T > using SHT1 = SingletonHolder<T, CreateUsingNew, DefaultLifetime, SingleThreaded>;
template< class T > using SHT2 = SingletonHolder<T, CreateUsingNew, PhoenixLifetime, SingleThreaded>;

A& a = SHT1<A>::Instance();
A& b = SHT1<A>::Instance();

assert(a==b);

A& c = SHT2<A>::Instance();
A& d = SHT2<A>::Instance();

assert(c==d);
assert(a!=c);

Заголовочный файл шаблона SingletonHolder и пример его применения доступны по ссылкам:

https://drive.google.com/file/d/0B1OQiuLaD--sY3FkLXdEVE1aX0E/view?usp=sharing
https://drive.google.com/file/d/0B1OQiuLaD--sQlVrd2FKUXM3ZE0/view?usp=sharing


 Используемые материалы:

[1] А.Александреску  “Современное проектирование на С++: Обобщенное программирование и прикладные шаблоны проектирования”
[3] Эрих Гамма  и др. «Приёмы объектно-ориентированного проектирования. Паттерны проектирования»
[4] Steve Ball and John Crawford "Monostate classes: the power of one” (http://www.cs.wustl.edu/~schmidt/monostate.rtf)
[5] D. Levine and C. Gill "Object Lifetime Manager. A Complementary Pattern for Controlling Object Creation and Destruction”.
[6] S. Meyers “More Effective C++” Item 26: Limiting the number of objects of a class.
[9] D. Schmidt and T. Harrison. “Double-Checked Locking. An Optimization Pattern for Efficiently Initializing and Accessing Thread-safe Objects.”

Комментарии

Популярные сообщения из этого блога

Полезные новации С++17. Выражения свертки (Fold expressions)

Перегрузка и специализация шаблонных функций.

Поддержка декомпозиции при объявлении (structural bindings) для классов