Является ли реализация Meyers потоком шаблонов Singleton безопасным?
Является ли следующая реализация, использующая ленивую инициализацию, безопасным потоком Singleton
(Meyers 'Singleton)?
static Singleton& instance()
{
static Singleton s;
return s;
}
Если нет, почему и как сделать его потокобезопасным?
Ответы
Ответ 1
В C++11 он является потокобезопасным. В соответствии со стандартом §6.7 [stmt.dcl] p4
:
Если элемент управления входит в объявление одновременно, когда переменная инициализируется, параллельное выполнение должно ждать для завершения инициализации.
Поддержка GCC и VS для функции (Динамическая инициализация и уничтожение с помощью Concurrency, также известная как Magic Statics на MSDN) выглядит следующим образом:
Спасибо @Mankarse и @olen_gam за их комментарии.
В C++03 этот код не был потокобезопасным. Существует статья Мейерса, называемая "С++ и опасностями блокировки с двойной проверкой" , в которой обсуждаются поточно-безопасные реализации шаблона, и вывод, более или менее, что (в С++ 03) полная блокировка вокруг метода создания является в основном простейшим способом обеспечения правильного concurrency на всех платформах, в то время как большинство форм двойных проверок вариантов шаблонов могут пострадать от условия гонки на определенных архитектурах, если только команды не чередуются с стратегически мерами памяти.
Ответ 2
Чтобы ответить на вопрос о том, почему он не является потоковым, это не потому, что первый вызов 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)));
}
Итак, вот простой потокобезопасный Singleton (для Windows). Он использует простую оболочку класса для объекта Windows CRITICAL_SECTION, так что мы можем заставить компилятор автоматически инициализировать CRITICAL_SECTION
до вызова 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 lock
// it 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 для обеспечения независимой от платформы оболочки для блокировки.
- это гарантирует безопасность потоков, когда экземпляр Singleton запрашивается после вызова
main()
- если вы вызываете его до этого (например, при инициализации статического объекта), все может не работать, потому что CRITICAL_SECTION
может не быть инициализирован.
- блокировка должна выполняться каждый раз, когда запрашивается экземпляр. Как я уже сказал, это простая поточная реализация. Если вам нужен лучший (или хотите знать, почему такие вещи, как метод блокировки двойной проверки, являются ошибочными), см. документы, связанные с ответом Гроо.
Ответ 3
Глядя на следующий стандарт (раздел 6.7.4), он объясняет, насколько статическая локальная инициализация является потокобезопасной. Поэтому, когда этот раздел стандарта будет широко реализован, Meyer Singleton станет предпочтительной реализацией.
Я уже не согласен со многими ответами. Большинство компиляторов уже реализуют статическую инициализацию таким образом. Одним из примечательных исключений является Microsoft Visual Studio.
Ответ 4
Правильный ответ зависит от вашего компилятора. Он может решить сделать это потокобезопасным; это не "naturallly" threadsafe.
Ответ 5
Является ли следующая реализация [...] потоком безопасной?
На большинстве платформ это не является потокобезопасным. (Приложите обычный отказ от ответственности, объяснив, что стандарт С++ не знает о потоках, поэтому на законных основаниях он не говорит, является это или нет.)
Если нет, то почему [...]?
Причина не в том, что ничто не мешает нескольким потокам одновременно выполнять конструктор s
'.
как сделать его потокобезопасным?
"С++ и опасения блокировки с двойным проверением" Скотта Мейерса и Андрея Александреску - довольно хороший трактат по теме нити - безопасные синглтоны.
Ответ 6
Как сказал MSalters: это зависит от используемой вами реализации на С++. Проверьте документацию. Что касается другого вопроса: "Если нет, то почему?" - Стандарт С++ ничего не говорит о потоках. Но предстоящая версия С++ знает потоки, и она явно заявляет, что инициализация статических локальных сетей является потокобезопасной. Если два потока вызывают такую функцию, один поток выполняет инициализацию, а другой будет блокировать и ждать завершения.