эффективный потокобезопасный синглтон в C++
обычный шаблон для одноэлементного класса-это что-то вроде
static Foo &getInst()
{
static Foo *inst = NULL;
if(inst == NULL)
inst = new Foo(...);
return *inst;
}
однако я понимаю, что это решение не является потокобезопасным, поскольку 1) конструктор Foo может быть вызван более одного раза (что может или не может иметь значения) и 2) inst не может быть полностью построен до того, как он будет возвращен в другой поток.
одним из решений является обертывание мьютекса вокруг всего метода, но тогда я плачу за накладные расходы синхронизации еще долго после того, как мне это действительно нужно. Один альтернатива-это что-то вроде
static Foo &getInst()
{
static Foo *inst = NULL;
if(inst == NULL)
{
pthread_mutex_lock(&mutex);
if(inst == NULL)
inst = new Foo(...);
pthread_mutex_unlock(&mutex);
}
return *inst;
}
это правильный способ сделать это, или есть какие-то подводные камни о которых мне следует знать? Например, существуют ли какие-либо проблемы с порядком статической инициализации, которые могут возникнуть, т. е. всегда ли Inst гарантированно будет NULL при первом вызове getInst?
9 ответов:
ваше решение называется "двойной проверкой блокировки", и то, как вы его написали, не является потокобезопасным.
этой Мейерс/Александреску бумаги объясняет, почему-но эта статья также широко неправильно понимается. Он запустил мем " двойная проверка блокировки небезопасна в C++", но его фактический вывод заключается в том, что двойная проверка блокировки в C++ может быть реализована безопасно, она просто требует использования барьеров памяти в неочевидном месте.
документ содержит псевдокод демонстрирует, как использовать барьеры памяти для безопасной реализации DLCP, поэтому вам не составит труда исправить вашу реализацию.
Если вы используете C++11, вот правильный способ сделать это:
Foo& getInst() { static Foo inst(...); return inst; }согласно новому стандарту нет необходимости заботиться об этой проблеме больше. Инициализация объекта будет производиться только одним потоком, остальные потоки будут ждать его завершения. Или вы можете использовать std::call_once. (более подробная информация здесь)
Херб Саттер говорит о двойной проверке блокировки в CppCon 2014.
Ниже приведен код, который я реализовал на C++11 на основе этого:
class Foo { public: static Foo* Instance(); private: Foo() {} static atomic<Foo*> pinstance; static mutex m_; }; atomic<Foo*> Foo::pinstance { nullptr }; std::mutex Foo::m_; Foo* Foo::Instance() { if(pinstance == nullptr) { lock_guard<mutex> lock(m_); if(pinstance == nullptr) { pinstance = new Foo(); } } return pinstance; }вы также можете проверить полную программу здесь:http://ideone.com/olvK13
использовать
pthread_once, что гарантирует, что функция инициализации выполняется один раз атомарно.(на Mac OS X он использует спин-замок. Не знаю реализации других платформ.)
TTBOMK, единственным гарантированным потокобезопасным способом сделать это без блокировки было бы инициализировать все ваши синглеты до вы когда-нибудь запустить поток.
ваша альтернатива называется "двойная проверка блокировки".
могут существовать многопоточные модели памяти, в которых он работает, но POSIX не гарантирует один
реализация Ace singleton использует дважды проверенный шаблон блокировки для потокобезопасности, вы можете обратиться к нему, если хотите.
вы можете найти исходный код здесь.
работает ли здесь TLS? https://en.wikipedia.org/wiki/Thread-local_storage#C_and_C++
например,
static _thread Foo *inst = NULL; static Foo &getInst() { if(inst == NULL) inst = new Foo(...); return *inst; }но нам также нужен способ удалить его явно, например
static void deleteInst() { if (!inst) { return; } delete inst; inst = NULL; }
решение не является потокобезопасным, потому что оператор
inst = new Foo();можно разбить на два оператора компилятором:
inst = malloc(sizeof(Foo)); inst->Foo();Так что если после выполнения оператора 1 потоком другой поток выполняет
getInstance()метод тогда он найдет, что указатель не является нулевым, а затем вернет указатель на неинициализированный объект.
Comments