Является ли реализация 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: это зависит от используемой вами реализации на С++. Проверьте документацию. Что касается другого вопроса: "Если нет, то почему?" - Стандарт С++ ничего не говорит о потоках. Но предстоящая версия С++ знает потоки, и она явно заявляет, что инициализация статических локальных сетей является потокобезопасной. Если два потока вызывают такую ​​функцию, один поток выполняет инициализацию, а другой будет блокировать и ждать завершения.