Подтвердить что ты не робот

Эффективный потокобезопасный синглтон в С++

Обычный шаблон для одноэлементного класса похож на

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;    
}

Это правильный способ сделать это, или есть ли какие-то подводные камни, о которых я должен знать? Например, существуют ли какие-либо проблемы с порядком статического инициализации, которые могут возникнуть, т.е. Всегда гарантированно будет NULL при первом вызове getInst?

4b9b3361

Ответ 1

Ваше решение называется "дважды проверенная блокировка", и так, как вы написали, это не потокобезопасно.

Этот материал Майерса/Александреску объясняет, почему, - но эта статья также широко непонятна. Он запустил "двойную проверенную блокировку" в С++ "meme", но ее фактический вывод состоит в том, что двойная проверенная блокировка на С++ может быть реализована безопасно, она просто требует использования барьеров памяти в неочевидном месте.

В документе содержится псевдокод, демонстрирующий использование барьеров памяти для безопасного внедрения DLCP, поэтому вам не составит труда исправить вашу реализацию.

Ответ 2

Если вы используете С++ 11, вот правильный способ сделать это:

Foo& getInst()
{
    static Foo inst(...);
    return inst;
}

В соответствии с новым стандартом больше не нужно заботиться об этой проблеме. Инициализация объекта будет выполняться только одним потоком, другие потоки будут ждать завершения. Или вы можете использовать std:: call_once. (подробнее здесь)

Ответ 3

Herb Sutter рассказывает о двойной проверке блокировки в CppCon 2014.

Ниже приведен код, реализованный на С++ 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

Ответ 4

Используйте pthread_once, что гарантируется, что функция инициализации запускается один раз атомарно.

(В Mac OS X используется блокировка спина. Не знаю, как реализовать другие платформы.)

Ответ 5

TTBOMK, единственный гарантированный потокобезопасный способ сделать это без блокировки - это инициализировать все ваши синглеты, прежде чем вы начнете поток.

Ответ 6

Ваша альтернатива называется "дважды проверенная блокировка" .

Могут существовать многопоточные модели памяти, в которых он работает, но POSIX не гарантирует один

Ответ 7

Реализация ACE singleton использует дважды проверенный шаблон блокировки для безопасности потоков, вы можете ссылаться на него, если хотите.

Вы можете найти исходный код здесь.

Ответ 8

Работает ли 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;
}

Ответ 9

Решение не является потокобезопасным, поскольку утверждение

inst = new Foo();

может быть разбит на два оператора компилятора:

inst = malloc(sizeof(Foo));   
inst->Foo();

Поэтому, если после выполнения инструкции 1 потоком другой поток выполняет метод getInstance() то он обнаружит, что указатель не является нулевым, а затем вернет указатель на неинициализированный объект.