эффективный потокобезопасный синглтон в 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?

839   9  

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

    Ничего не найдено.