Czy implementacja Meyersa wątku wzorca Singleton jest bezpieczna?

145

Czy poniższa implementacja Singletonwątku (Meyers 'Singleton) przy użyciu leniwej inicjalizacji jest bezpieczna?

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

Jeśli nie, dlaczego i jak zabezpieczyć wątek?

Ankur
źródło
Czy ktoś może wyjaśnić, dlaczego nie jest to bezpieczne dla wątków. Artykuły wymienione w linkach omawiają bezpieczeństwo wątków przy użyciu alternatywnej implementacji (przy użyciu zmiennej wskaźnikowej, tj. Statycznej Singleton * pInstance).
Ankur

Odpowiedzi:

168

W C ++ 11 jest bezpieczny wątkowo. Zgodnie z normą , §6.7 [stmt.dcl] p4:

Jeśli sterowanie wprowadzi deklarację jednocześnie podczas inicjalizacji zmiennej, równoczesne wykonanie będzie czekało na zakończenie inicjalizacji.

Obsługa GCC i VS dla tej funkcji ( Dynamic Initialization and Destruction with Concurrency , znana również jako Magic Statics w MSDN ) jest następująca:

Dzięki @Mankarse i @olen_gam za ich komentarze.


W C ++ 03 ten kod nie był bezpieczny dla wątków. Jest artykuł Meyersa zatytułowany „C ++ and the Perils of Double-Checked Locking”, który omawia bezpieczne wątkowo implementacje wzorca, a wniosek jest mniej więcej taki, że (w C ++ 03) pełne blokowanie wokół metody tworzenia instancji jest w zasadzie najprostszym sposobem zapewnienia właściwej współbieżności na wszystkich platformach, podczas gdy większość form podwójnie sprawdzonych wariantów wzorca blokowania może cierpieć z powodu warunków wyścigu na niektórych architekturach , chyba że instrukcje są przeplatane strategicznie rozmieszczonymi barierami pamięci.

Groo
źródło
3
Istnieje również obszerna dyskusja na temat wzorca Singleton (żywotność i bezpieczeństwo wątków) autorstwa Alexandrescu w Modern C ++ Design. Zobacz witrynę Lokiego: loki-lib.sourceforge.net/index.php?n=Pattern.Singleton
Matthieu M.
1
Możesz utworzyć singleton bezpieczny dla wątków za pomocą boost :: call_once.
CashCow
1
Niestety ta część standardu nie jest zaimplementowana w kompilatorze Visual Studio 2012 C ++. W tabeli „C ++ 11 Core Language Features: Concurrency” określane jako „Magic Statics”: msdn.microsoft.com/en-us/library/vstudio/hh567368.aspx
olen_garn
Fragment standardu dotyczy konstrukcji, ale nie zniszczenia. Czy standard zapobiega zniszczeniu obiektu w jednym wątku, podczas gdy (lub przed) inny wątek próbuje uzyskać do niego dostęp po zakończeniu programu?
stewbasic
IANA (język C ++) L, ale sekcja 3.6.3 [basic.start.term] p2 sugeruje, że możliwe jest uderzenie w niezdefiniowane zachowanie, próbując uzyskać dostęp do obiektu po jego zniszczeniu?
stewbasic
21

Odpowiadając na pytanie, dlaczego nie jest to wątkowo bezpieczne, nie dzieje się tak dlatego, że pierwsze wywołanie instance()musi wywołać konstruktora Singleton s. Aby zapewnić bezpieczeństwo wątków, musiałoby to nastąpić w sekcji krytycznej, ale standard nie wymaga, aby została wybrana sekcja krytyczna (dotychczasowy standard całkowicie milczy na temat wątków). Kompilatory często implementują to za pomocą prostego sprawdzenia i inkrementacji statycznej wartości logicznej - ale nie w krytycznej sekcji. Coś w rodzaju następującego pseudokodu:

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

Oto prosty, bezpieczny wątkowo Singleton (dla Windows). Używa prostego opakowania klasy dla obiektu CRITICAL_SECTION systemu Windows, dzięki czemu kompilator może automatycznie zainicjować CRITICAL_SECTIONpoprzednią main()wywołanie. Idealnie byłoby użyć prawdziwej klasy sekcji krytycznej RAII, która może obsługiwać wyjątki, które mogą wystąpić, gdy sekcja krytyczna jest utrzymywana, ale to wykracza poza zakres tej odpowiedzi.

Podstawową operacją jest to, że gdy Singletonżądane jest wystąpienie of , zostaje przyjęta blokada, tworzony jest singleton, jeśli to konieczne, a następnie blokada jest zwalniana i zwracane jest odniesienie do singletona.

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

Człowieku - to dużo bzdur, żeby „uczynić świat lepszym”.

Główne wady tej implementacji (jeśli nie przepuściłem kilku błędów) to:

  • jeśli zostanie new Singleton()rzucony, blokada nie zostanie zwolniona. Można to naprawić, używając prawdziwego obiektu blokady RAII zamiast prostego, który mam tutaj. Może to również pomóc w uczynieniu rzeczy przenośnymi, jeśli używasz czegoś takiego jak Boost, aby zapewnić niezależne od platformy opakowanie blokady.
  • Gwarantuje to bezpieczeństwo wątków, gdy instancja Singleton jest żądana po main()wywołaniu - jeśli wywołasz ją wcześniej (jak w przypadku inicjalizacji obiektu statycznego), rzeczy mogą nie działać, ponieważ CRITICAL_SECTIONmoże nie zostać zainicjowany.
  • blokada musi zostać podjęta za każdym razem, gdy żądana jest instancja. Jak powiedziałem, jest to prosta implementacja bezpieczna dla wątków. Jeśli potrzebujesz lepszego (lub chcesz wiedzieć, dlaczego takie rzeczy jak technika podwójnego sprawdzania blokady są wadliwe), zapoznaj się z dokumentami, do których odnosi się odpowiedź Groo .
Michael Burr
źródło
1
O o. Co się stanie, jeśli new Singleton()rzuci?
sbi
@Bob - żeby być uczciwym, z odpowiednim zestawem bibliotek, wszystkie okrucieństwa związane z brakiem możliwości kopiowania i odpowiednią blokadą RAII zniknęłyby lub byłyby minimalne. Ale chciałem, żeby przykład był w miarę samodzielny. Mimo, że singletony to dużo pracy z może minimalnym zyskiem, uznałem je za przydatne w zarządzaniu użyciem globali. Zwykle ułatwiają ustalenie, gdzie i kiedy są używane, trochę lepiej niż zwykła konwencja nazewnictwa.
Michael Burr
@sbi: w tym przykładzie, jeśli new Singleton()rzuca, zdecydowanie występuje problem z blokadą. Należy użyć odpowiedniej klasy blokady RAII, coś w rodzaju lock_guardBoost. Chciałem, żeby przykład był mniej lub bardziej samodzielny, a był już trochę potworem, więc odrzuciłem bezpieczeństwo wyjątków (ale je odwołałem). Może powinienem to naprawić, aby ten kod nie został wycięty i wklejony w niewłaściwym miejscu.
Michael Burr
Po co dynamicznie przydzielać singleton? Dlaczego po prostu nie uczynić „pInstance” statycznym składnikiem „Singleton :: instance ()”?
Martin York
@Martin - gotowe. Masz rację, to sprawia, że ​​jest to trochę prostsze - byłoby jeszcze lepiej, gdybym użył klasy zamka RAII.
Michael Burr
10

Patrząc na następny standard (sekcja 6.7.4), wyjaśnia, jak statyczna inicjalizacja lokalna jest bezpieczna dla wątków. Kiedy więc ta sekcja standardu zostanie szeroko zaimplementowana, Meyer's Singleton będzie preferowaną implementacją.

Nie zgadzam się już z wieloma odpowiedziami. Większość kompilatorów już implementuje statyczną inicjalizację w ten sposób. Jedynym godnym uwagi wyjątkiem jest Microsoft Visual Studio.

deft_code
źródło
6

Prawidłowa odpowiedź zależy od Twojego kompilatora. Może zdecydować, aby był bezpieczny dla wątków; nie jest „naturalnie” bezpieczne dla wątków.

MSalters
źródło
5

Czy następujący wątek [...] implementacji jest bezpieczny?

Na większości platform nie jest to bezpieczne dla wątków. (Dołącz zwykłe zastrzeżenie wyjaśniające, że standard C ++ nie wie o wątkach, więc zgodnie z prawem nie mówi, czy tak jest, czy nie).

Jeśli nie, to dlaczego […]?

Przyczyną tego nie jest to, że nic nie stoi na przeszkodzie, aby więcej niż jeden wątek wykonywał jednocześnie skonstruktora.

jak zabezpieczyć wątek?

„C ++ and the Perils of Double-Checked Locking” Scotta Meyersa i Andrei Alexandrescu to całkiem niezły traktat na temat singletonów bezpiecznych dla wątków.

sbi
źródło
2

Jak powiedział MSalters: To zależy od używanej implementacji C ++. Sprawdź dokumentację. A jeśli chodzi o drugie pytanie: „Jeśli nie, to dlaczego?” - Standard C ++ nie wspomina jeszcze nic o wątkach. Jednak nadchodząca wersja C ++ jest świadoma wątków i wyraźnie stwierdza, że ​​inicjalizacja statycznych lokalizacji lokalnych jest bezpieczna dla wątków. Jeśli dwa wątki wywołują taką funkcję, jeden wątek wykona inicjalizację, podczas gdy drugi będzie blokował i czekał na zakończenie.

sellibitze
źródło