Singleton
Безопасна ли следующая реализация с использованием отложенной инициализации потока (Meyers 'Singleton)?
static Singleton& instance()
{
static Singleton s;
return s;
}
Если нет, то почему и как сделать это потокобезопасным?
Singleton
Безопасна ли следующая реализация с использованием отложенной инициализации потока (Meyers 'Singleton)?
static Singleton& instance()
{
static Singleton s;
return s;
}
Если нет, то почему и как сделать это потокобезопасным?
Ответы:
В C ++ 11 это потокобезопасно. Согласно стандарту , §6.7 [stmt.dcl] p4
:
Если элемент управления вводит объявление одновременно во время инициализации переменной, параллельное выполнение должно ожидать завершения инициализации.
GCC и VS поддерживают эту функцию ( динамическая инициализация и разрушение с параллелизмом , также известная как Magic Statics на MSDN ):
Спасибо @Mankarse и @olen_gam за их комментарии.
В C ++ 03 этот код не был потокобезопасным. Есть статья Мейерса под названием «C ++ и опасности двойной проверки блокировки», в которой обсуждаются поточно-ориентированные реализации шаблона, и делается вывод, что более или менее (в C ++ 03) полная блокировка вокруг экземпляра метода в основном это самый простой способ обеспечения надлежащего параллелизма на всех платформах, в то время как большинство форм проверенных вариантов шаблонов блокировки с двойной проверкой могут страдать от состояния гонки на определенных архитектурах , если инструкции не чередуются со стратегически размещаемыми барьерами памяти.
Чтобы ответить на ваш вопрос о том, почему это не потокобезопасно, это не потому, что первый вызов instance()
должен вызывать конструктор Singleton s
. Чтобы быть потокобезопасным, это должно происходить в критической секции, но в стандарте нет требования, чтобы критическая секция была принята (стандарт на сегодняшний день полностью ничего не говорит о потоках). Компиляторы часто реализуют это с помощью простой проверки и приращения статического логического значения - но не в критической секции. Что-то вроде следующего псевдокода:
static Singleton& instance()
{
static bool initialized = false;
static char s[sizeof( Singleton)];
if (!initialized) {
initialized = true;
new( &s) Singleton(); // call placement new on s to construct it
}
return (*(reinterpret_cast<Singleton*>( &s)));
}
Итак, вот простой потокобезопасный синглтон (для Windows). Он использует простую оболочку класса для объекта Windows CRITICAL_SECTION, так что мы можем сделать так, чтобы компилятор автоматически инициализировал вызов CRITICAL_SECTION
before main()
. В идеале должен использоваться настоящий класс критической секции RAII, который может иметь дело с исключениями, которые могут возникнуть при удержании критической секции, но это выходит за рамки этого ответа.
Основная операция заключается в том, что когда Singleton
запрашивается экземпляр , берется блокировка, при необходимости создается Singleton, затем блокировка освобождается и возвращается ссылка Singleton.
#include <windows.h>
class CritSection : public CRITICAL_SECTION
{
public:
CritSection() {
InitializeCriticalSection( this);
}
~CritSection() {
DeleteCriticalSection( this);
}
private:
// disable copy and assignment of CritSection
CritSection( CritSection const&);
CritSection& operator=( CritSection const&);
};
class Singleton
{
public:
static Singleton& instance();
private:
// don't allow public construct/destruct
Singleton();
~Singleton();
// disable copy & assignment
Singleton( Singleton const&);
Singleton& operator=( Singleton const&);
static CritSection instance_lock;
};
CritSection Singleton::instance_lock; // definition for Singleton's lock
// it's initialized before main() is called
Singleton::Singleton()
{
}
Singleton& Singleton::instance()
{
// check to see if we need to create the Singleton
EnterCriticalSection( &instance_lock);
static Singleton s;
LeaveCriticalSection( &instance_lock);
return s;
}
Человек - это много дерьма, чтобы «сделать мир лучше».
Основными недостатками этой реализации (если я не позволил некоторым ошибкам проскользнуть) является:
new Singleton()
бросает, замок не будет освобожден. Это можно исправить с помощью настоящего объекта блокировки RAII вместо простого, который у меня есть здесь. Это также может помочь сделать вещи переносимыми, если вы используете что-то вроде Boost для предоставления независимой от платформы оболочки для блокировки.main()
вызова - если вы вызываете его до этого (как в случае инициализации статического объекта), вещи могут не работать, потому что они CRITICAL_SECTION
могут быть не инициализированы.new Singleton()
бросит?
new Singleton()
выбрасывает, определенно проблема с блокировкой. Нужно использовать правильный класс блокировки RAII, что-то вроде lock_guard
Boost. Я хотел, чтобы пример был более или менее самодостаточным, и он уже был чем-то вроде монстра, поэтому я остановил исключение безопасности (но вызвал его). Может быть, я должен исправить это, чтобы этот код не вставлялся где-то неподходящим образом.
Глядя на следующий стандарт (раздел 6.7.4), он объясняет, насколько статическая локальная инициализация является поточно-ориентированной. Поэтому, как только этот раздел стандарта будет широко внедрен, предпочтение отдается Мейеру Синглтону.
Я не согласен со многими ответами уже. Большинство компиляторов уже реализуют статическую инициализацию таким образом. Единственным заметным исключением является Microsoft Visual Studio.
Безопасна ли следующая реализация [...] для потока?
На большинстве платформ это не потокобезопасно. (Добавьте обычный отказ от ответственности, пояснив, что стандарт C ++ не знает о потоках, поэтому по закону он не говорит, является ли он или нет.)
Если нет, то почему [...]?
Причина этого заключается в том, что ничто не мешает более чем одному потоку одновременно выполнять s
конструктор.
как сделать это потокобезопасным?
«C ++ и опасности двойной проверки блокировки» Скотта Мейерса и Андрея Александреску - довольно хороший трактат на тему поточно-ориентированных синглетонов.
Как сказал MSalters: это зависит от используемой вами реализации C ++. Проверьте документацию. Что касается другого вопроса: «Если нет, то почему?» - Стандарт C ++ пока ничего не упоминает о потоках. Но будущая версия C ++ знает о потоках и явно заявляет, что инициализация статических локальных объектов является поточно-ориентированной. Если два потока вызывают такую функцию, один поток выполнит инициализацию, а другой заблокирует и дождется ее завершения.