Безопасна ли реализация Мейерсом потока шаблона Singleton?

145

SingletonБезопасна ли следующая реализация с использованием отложенной инициализации потока (Meyers 'Singleton)?

static Singleton& instance()
{
     static Singleton s;
     return s;
}

Если нет, то почему и как сделать это потокобезопасным?

Анкур
источник
Может кто-нибудь объяснить, почему это не потокобезопасно. Статьи, упомянутые в ссылках, обсуждают безопасность потоков, используя альтернативную реализацию (используя переменную-указатель, т.е. статический Singleton * pInstance).
Анкур
1
См .: stackoverflow.com/questions/449436/…
Мартин Йорк,

Ответы:

168

В C ++ 11 это потокобезопасно. Согласно стандарту , §6.7 [stmt.dcl] p4:

Если элемент управления вводит объявление одновременно во время инициализации переменной, параллельное выполнение должно ожидать завершения инициализации.

GCC и VS поддерживают эту функцию ( динамическая инициализация и разрушение с параллелизмом , также известная как Magic Statics на MSDN ):

Спасибо @Mankarse и @olen_gam за их комментарии.


В C ++ 03 этот код не был потокобезопасным. Есть статья Мейерса под названием «C ++ и опасности двойной проверки блокировки», в которой обсуждаются поточно-ориентированные реализации шаблона, и делается вывод, что более или менее (в C ++ 03) полная блокировка вокруг экземпляра метода в основном это самый простой способ обеспечения надлежащего параллелизма на всех платформах, в то время как большинство форм проверенных вариантов шаблонов блокировки с двойной проверкой могут страдать от состояния гонки на определенных архитектурах , если инструкции не чередуются со стратегически размещаемыми барьерами памяти.

Гру
источник
3
Существует также обширное обсуждение паттерна Singleton (время жизни и безопасность потоков), автором которого является Alexandrescu в Modern C ++ Design. См. Сайт Локи: loki-lib.sourceforge.net/index.php?n=Pattern.Singleton
Matthieu M.
1
Вы можете создать потокобезопасный синглтон с помощью boost :: call_once.
CashCow
1
К сожалению, эта часть стандарта не реализована в компиляторе Visual Studio 2012 C ++. В таблице «Основные языковые возможности C ++ 11: параллелизм» упоминается как «Волшебная статика» здесь: msdn.microsoft.com/en-us/library/vstudio/hh567368.aspx
olen_garn
Фрагмент из стандарта касается конструкции, но не разрушения. Предотвращает ли стандарт уничтожение объекта в одном потоке, когда (или до этого) другой поток пытается получить к нему доступ при завершении программы?
тушеное мясо
IANA (язык C ++) L, но в разделе 3.6.3 [basic.start.term] p2 предполагается, что можно поразить неопределенное поведение, пытаясь получить доступ к объекту после его уничтожения?
тушеное мясо
21

Чтобы ответить на ваш вопрос о том, почему это не потокобезопасно, это не потому, что первый вызов 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)));
}

Итак, вот простой потокобезопасный синглтон (для Windows). Он использует простую оболочку класса для объекта Windows CRITICAL_SECTION, так что мы можем сделать так, чтобы компилятор автоматически инициализировал вызов CRITICAL_SECTIONbefore 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's lock
                                      //  it's 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могут быть не инициализированы.
  • блокировка должна быть взята каждый раз, когда запрашивается экземпляр. Как я уже сказал, это простая многопоточная реализация. Если вам нужен лучший (или вы хотите знать, почему такие вещи, как техника двойной проверки блокировки, ошибочны), см. Статьи, ссылки на которые есть в ответе Гроо .
Майкл Берр
источник
1
Ооо Что будет, если new Singleton()бросит?
ВОО
@Bob - честно говоря, с надлежащим набором библиотек, все бесполезность, связанная с невозможностью копирования и надлежащей блокировкой RAII, исчезнет или будет минимальной. Но я хотел, чтобы пример был достаточно автономным. Несмотря на то, что синглтоны - это много работы, возможно, с минимальной прибылью, я нашел их полезными для управления использованием глобалов. Они, как правило, облегчают поиск того, где и когда они используются, немного лучше, чем просто соглашение об именах.
Майкл Берр
@sbi: в этом примере, если new Singleton()выбрасывает, определенно проблема с блокировкой. Нужно использовать правильный класс блокировки RAII, что-то вроде lock_guardBoost. Я хотел, чтобы пример был более или менее самодостаточным, и он уже был чем-то вроде монстра, поэтому я остановил исключение безопасности (но вызвал его). Может быть, я должен исправить это, чтобы этот код не вставлялся где-то неподходящим образом.
Майкл Берр
Зачем динамически выделять синглтон? Почему бы просто не сделать pInstance статическим членом Singleton :: instance ()?
Мартин Йорк,
@ Мартин - готово. Вы правы, это немного упрощает - было бы еще лучше, если бы я использовал класс блокировки RAII.
Майкл Берр
10

Глядя на следующий стандарт (раздел 6.7.4), он объясняет, насколько статическая локальная инициализация является поточно-ориентированной. Поэтому, как только этот раздел стандарта будет широко внедрен, предпочтение отдается Мейеру Синглтону.

Я не согласен со многими ответами уже. Большинство компиляторов уже реализуют статическую инициализацию таким образом. Единственным заметным исключением является Microsoft Visual Studio.

deft_code
источник
6

Правильный ответ зависит от вашего компилятора. Он может решить сделать его потокобезопасным; это не "естественно" потокобезопасно.

MSalters
источник
5

Безопасна ли следующая реализация [...] для потока?

На большинстве платформ это не потокобезопасно. (Добавьте обычный отказ от ответственности, пояснив, что стандарт C ++ не знает о потоках, поэтому по закону он не говорит, является ли он или нет.)

Если нет, то почему [...]?

Причина этого заключается в том, что ничто не мешает более чем одному потоку одновременно выполнять sконструктор.

как сделать это потокобезопасным?

«C ++ и опасности двойной проверки блокировки» Скотта Мейерса и Андрея Александреску - довольно хороший трактат на тему поточно-ориентированных синглетонов.

SBI
источник
2

Как сказал MSalters: это зависит от используемой вами реализации C ++. Проверьте документацию. Что касается другого вопроса: «Если нет, то почему?» - Стандарт C ++ пока ничего не упоминает о потоках. Но будущая версия C ++ знает о потоках и явно заявляет, что инициализация статических локальных объектов является поточно-ориентированной. Если два потока вызывают такую ​​функцию, один поток выполнит инициализацию, а другой заблокирует и дождется ее завершения.

sellibitze
источник