Pull to refresh

Comments 17

Заголовок слегка вводит в заблуждение: я сначала подумал, что имеется в виду классическая бяка про одновременное создание синглтона из двух тредов, а у вас — про преждевременное удаление синглтона.
Кстати, а если в паре с синглтоном использовать Schwarz Counter? Синглон удалится после того, как отработают деструкторы статических объектов для всех единиц трансляции, в которых используется синглтон — причем сам синглтон можно сделать даже на сыром указателе. Разве это не оно самое?
С одновременным созданием из разных потоков проблемы как бы нет совсем — стандарт гарантирует отсутствие гонки. Если интересно — дам ссылки на пункты стандарта. Хотя это и выходит за рамки статьи.
Просто с созданием — тоже нет проблемы, т.к. сам по себе паттерн «Синглтон» именно и направлен на то, чтобы статик инициализировался при первом обращении.
А вот с уничтожением — просто беда. Говорю из личного опыта: примерно половина случаев применения синглтона требовала дебага, т.к. при сворачивании приложения происходил крэш. В статье показано, что, если о возможных проблемах не задумываться, то оно примерно так и будет, т.к. зависит от казалось бы незначительных факторов.

«The 'nifty counter' or 'Schwarz counter' idiom is an example of a reference counting idiom». Идиома подсчёта ссылок уже реализована в STL. Причём в дополнение к std::shared_ptr есть еще и std::weak_ptr, который позволяет делать очень замечательные вещи. В статье полезность std::weak_ptr я раскрыл аж в двух примерах — последний пример из классики, и корректный пример из SingletonWeak. Кроме того, std::shared_ptr гарантирует потокобезопасность блока подсчёта ссылок (хотя, опять же, это выходит за рамки статьи). Итого, упомянутая Вами идиома:
— требует ручной реализации того, что уже есть в STL;
— чтобы сделать её потокобезопасной, придётся попотеть;
— предполагает только продление жизни, недостатки которого я раскрыл достаточно подробно.
Там используется другая идея — разделение компилятором инициализации статиков на две фазы: статическую и динамическую. Преимуществ относительно применения STL не просматривается.
А разве при использовании weak pointer вызывающий код не должен каждый раз вызывать lock() и проверять результат? С удалением синглтона по счетчику пойнтер можно использовать сразу. Правда, там под капотом другого оверхеда дофига. :)
Не понял вопроса. Уточните.
Использование синглтона, возвращающего сырой указатель или shared_ptr:
Singleton::instance()->use();

При этом валидность указателя обеспечивается за счет счетчика Шварца.

Использование синглтона, возвращающего weak_ptr:
if (auto ptr = Singleton::instance().lock())
     ptr->use();
else
    // протух

Это ж в каждом месте, где используется синглтон, придется городить if.
Если Вы это приводите, как аргумент в пользу счётчика Шварца — то какой-то он очень уж слабенький. Вообще к любому указателю, даже сырому, лучше не обращаться без проверки.
Счётчик Шварца является старомодной идиомой на сырых указателях без потокобезопасности, использующей для той же задачи несколько объектов со static storage duration. Возможно, его тоже можно улучшить с применением современного STL — напишите свою статью об этом, там и обсудим более подробно. В нём намного больше магии, чем в синглтонах Мейерса и их модификациях, рассмотренных в статье. Например (код отсюда):
static struct StreamInitializer {
  StreamInitializer ();
  ~StreamInitializer ();
} streamInitializer; // static initializer for every translation unit

Достаточно забыть «streamInitializer» перед завершающей точкой с запятой (при реализации без шпаргалки наверняка так и будет) — и эта идиома может несколько раз создать-уничтожить синглтон, что вряд ли соответствует задумке. Хотя как-то работать при этом будет, и о такой ошибке Вас никто не предупредит — ни компилятор, ни даже сторонний статический анализатор.
Если углубляться в область «какие ошибки можно сделать» — мы погрязнем в придуманных ситуациях. Так-то любой синглтон можно реализовать криво или удалить объект по указателю напрямую. :)

Собственно, я не настаиваю на том, что счетчик — это панацея (и примерно представляю, когда он не сработает). Просто вспомнился как вариант.
Вариант, да.
Но в плане реальной применимости:
— реализовать идиому Шварца по памяти через месяц после прочтения — вряд ли реально, а приведённые мной модификации — вполне;
— как поддерживать код, для правильного функционирования которого требуется столько весьма точно выполненных ритуалов?
— требуемый для корректировки синглтона Мейерса механизм (умные указатели) УЖЕ давно стандартизован, и отказываться от него в пользу самодельного велосипеда — ну я прямо даже не знаю…
И самое главное — ради чего? Где преимущества?
— как поддерживать код, для правильного функционирования которого требуется столько весьма точно выполненных ритуалов?

Вот как раз с поддержкой проблем не возникает — [правильно написанный] счетчик автоматически появляется и начинает считать, когда в исходник включается соответствующий *.h файл.
Зато могут возникнуть проблемы с пониманием, когда человек откроет сей хедер и увидит там вроде бы неправильную для хедера вещь (создание объекта).

— требуемый для корректировки синглтона Мейерса механизм (умные указатели) УЖЕ давно стандартизован, и отказываться от него в пользу самодельного велосипеда — ну я прямо даже не знаю…

С одной стороны — да. С другой стороны, счетчик срабатывает только при старте и стопе приложения, а использование shared_ptr наложит свой оверхед на все время выполнения. Если учесть, что боремся мы с глюками именно при стопе приложения — оверхед может быть нежелательным.

И самое главное — ради чего? Где преимущества?

Преимущество в том, что можно использовать сырые указатели и не считать инстансы в рантайме — а это может быть критично в многопоточной среде при частых вызовах instance().
Недостаток — в том, что счетчик не гарантирует существование объекта при хитрых сценариях использования. :)
Данная статья в первую очередь про избежание неопределённого поведения.
Но теперь мне хотя бы понятен довод: производительность. Сравнение которой еще и в многопоточном исполнении после устранения гонок в оригинальном счётчике Шварца вполне потянет на отдельную статью.
Предлагаю завязывать — обсуждение ни о чём.
Статья и предлагаемые решения замечательна, но существует все же более удачное решение. Исходное решение заключается в опоре на детали, на стандарты, в опоре на договорное поведение (которое на некоторых платформах в некоторых случаях все же нарушается) и прочей зависимости от «хитроумных», скрытых и договорных деталей.
Так делать не надо.
Поощряет к этому по моему мнению еще то, что последние стандарты C++ — с недостаточно ясными целями пытаются взвалить на себя (обернуть) OS, hardware и платформенно-специфичные механизмы, которые обычно уже не один десяток лет решены чисто и строго.

Некоторые индустриальные стандарты (MISRA) не поощряют конструирование систем на подобных принципах.

Многие индустриальные решения (RTOS-системы) в явном виде содержат фреймворки содержащие внутри себя init-active-deinit фазы жизни модулей/объектов итд.

У множественных синглтонов существует генеральный недостаток:
отсутствие единого централизованного управления (скрытый менеджмент, отсутствие управляемости, разнесенный, «размазанный» код).

Вместо подобных конструктивов жизнь в итоге научила меня проектировать все свои проекты/системы с учетом трёх фаз «жизни»:
1. Инициализация.
2. Рабочее состояние.
3. Деинициализация.

Внутри проектов могут существовать синглтоны, но по другим причинам — синглтон я рассаматриваю как вырожденую «фабрику», всегда отдающую один и тот же объект для того чтобы сделать код независимым от конкретных типов. Однако, все эти синглтоны (сам тип) инициализируются и деинициализируются строго в специально предназначенное для этого время. Инициализация/деинициализация недоступна обычному (его можно называть «user-level») коду. Дополнительным преимуществом является то что «user-level» слой кода всегда гарантированно получит instance, проверять ему не требуется, иного быть не может.

Возникает вопрос, но как быть если требуется динамически создавать тяжеловесные объекты по ходу жизни? Эти задачи решаются не синглтоном (не разбросанными по проекту и проектам синглтонами), а фабрикой. Фабрики построены на тех же принципах времени жизни что и тип синглтона, инициализируются строго до запуска слоя «user-level» кода и деинициализируются после него. Но кроме этого фабрики (или одна фабрика) осуществляют внутренний трекинг создаваемых объектов, что позволяет им гарантированно освободить их на фазе деинициализации.

Желаю всем управляемого кода.
Ну я же просил — не выходить за рамки статьи. Она НЕ об архитекуре вообще. Она НЕ о недостатках синглтона как архитектурного решения.Она НЕ о возможных альтернативах. Может быть немного она «о возможных последствиях нескольких синглтонов в одной программе, и что с этим по-быстрому сделать, чтобы не перекраивать архитектуру».
Упомянутые «возможные последствия» вполне могут отпугнуть от использования паттерна (я даже надеюсь, что отпугнут).
Ну а тех, кого не отпугнут — снабдят более глубоким пониманием.
Опять же, поднятые в статье вопросы, если Вы не заметили (допускаю, что статью толком не прочитали), как раз приближают к:
проектировать все свои проекты/системы с учетом трёх фаз «жизни»
и
Инициализация/деинициализация недоступна обычному (его можно называть «user-level») коду

Ещё более неуместно в данном контексте выглядит критика стандарта, если честно.
UFO just landed and posted this here
Да, весь материал статьи основан именно на синглтоне Мейерса и его модификациях.
Sign up to leave a comment.

Articles