эффективный потокобезопасный синглтон в C ++ - PullRequest
66 голосов
/ 05 апреля 2010

Обычный шаблон для одноэлементного класса - что-то вроде

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 ]

82 голосов
/ 11 ноября 2013

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

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

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

42 голосов
/ 05 апреля 2010

Ваше решение называется «двойная проверка блокировки», и, как вы написали, оно не является поточно-ориентированным.

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

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

11 голосов
/ 16 января 2015

Херб Саттер рассказывает о двойной проверке блокировки в 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

8 голосов
/ 05 апреля 2010

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

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

3 голосов
/ 05 апреля 2010

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

0 голосов
/ 07 июля 2018

Решение не является поточно-ориентированным, потому что утверждение

inst = new Foo();

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

Statement1: inst = malloc (sizeof (Foo));
Statement2: inst-> Foo ();

Предположим, что после выполнения оператора 1 одним потоком происходит переключение контекста. И второй поток также выполняет метод getInstance(). Затем второй поток обнаружит, что указатель 'inst' не является нулевым. Таким образом, второй поток вернет указатель на неинициализированный объект, так как первый поток еще не вызвал конструктор.

0 голосов
/ 15 апреля 2018

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;
}
0 голосов
/ 05 апреля 2010

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

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

0 голосов
/ 05 апреля 2010

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

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

...