Ещё раз о синглтоне
Настоящая статья написана под влиянием главы “Реализация
шаблона синглтон” из широко известной книги А. Александреску[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
Заголовочный файл шаблона 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.”
Комментарии
Отправить комментарий