Ist Meyers 'Implementierung des Singleton-Pattern-Threads sicher?

145

Ist die folgende Implementierung des Singleton(Meyers 'Singleton) -Threads mit verzögerter Initialisierung sicher?

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

Wenn nicht, warum und wie kann der Thread sicher gemacht werden?

Ankur
quelle
Kann mir bitte jemand erklären, warum dies nicht threadsicher ist. In den in den Links erwähnten Artikeln wird die Thread-Sicherheit mithilfe einer alternativen Implementierung (unter Verwendung einer Zeigervariable, dh statischer Singleton * pInstance) erläutert.
Ankur

Antworten:

168

In C ++ 11 ist es threadsicher. Gemäß dem Standard , §6.7 [stmt.dcl] p4:

Wenn die Steuerung gleichzeitig in die Deklaration eingibt, während die Variable initialisiert wird, wartet die gleichzeitige Ausführung auf den Abschluss der Initialisierung.

Die GCC- und VS-Unterstützung für die Funktion ( Dynamische Initialisierung und Zerstörung mit Parallelität , auch als Magic Statics auf MSDN bezeichnet ) lautet wie folgt:

Vielen Dank an @Mankarse und @olen_gam für ihre Kommentare.


In C ++ 03 war dieser Code nicht threadsicher. Es gibt einen Artikel von Meyers mit dem Titel "C ++ und die Gefahren des Double-Checked Locking", in dem threadsichere Implementierungen des Musters erörtert werden, und die Schlussfolgerung ist mehr oder weniger, dass (in C ++ 03) die Instanziierungsmethode vollständig gesperrt ist Dies ist im Grunde der einfachste Weg, um eine ordnungsgemäße Parallelität auf allen Plattformen sicherzustellen, während die meisten Formen von doppelt überprüften Sperrmustervarianten unter bestimmten Bedingungen unter Rennbedingungen leiden können , es sei denn, Anweisungen sind mit strategisch platzierten Speicherbarrieren verschachtelt.

Groo
quelle
3
Es gibt auch eine ausführliche Diskussion über das Singleton-Muster (Lebensdauer und Thread-Sicherheit) von Alexandrescu in Modern C ++ Design. Siehe Lokis Website: loki-lib.sourceforge.net/index.php?n=Pattern.Singleton
Matthieu M.
1
Sie können mit boost :: call_once einen thread-sicheren Singleton erstellen.
CashCow
1
Leider ist dieser Teil des Standards nicht im Visual Studio 2012 C ++ - Compiler implementiert. In der Tabelle "C ++ 11- Kernsprachenfunktionen
olen_garn
Der Ausschnitt aus dem Standard befasst sich mit der Konstruktion, aber nicht mit der Zerstörung. Verhindert der Standard, dass das Objekt in einem Thread zerstört wird, während (oder bevor) ein anderer Thread bei Programmbeendigung versucht, darauf zuzugreifen?
Stewbasic
IANA (C ++ - Sprache) L, aber Abschnitt 3.6.3 [basic.start.term] p2 schlägt vor, dass es möglich ist, undefiniertes Verhalten zu treffen, indem versucht wird, auf das Objekt zuzugreifen, nachdem es zerstört wurde?
Stewbasic
21

Um Ihre Frage zu beantworten, warum es nicht threadsicher ist, instance()muss der Konstruktor beim ersten Aufruf von nicht aufgerufen werden Singleton s. Um threadsicher zu sein, müsste dies in einem kritischen Abschnitt geschehen, und es gibt im Standard keine Anforderung, dass ein kritischer Abschnitt verwendet wird (der bisherige Standard enthält keine Informationen zu Threads). Compiler implementieren dies häufig durch einfaches Überprüfen und Inkrementieren eines statischen Booleschen Werts - jedoch nicht in einem kritischen Abschnitt. So etwas wie der folgende Pseudocode:

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

Hier ist also ein einfacher thread-sicherer Singleton (für Windows). Es verwendet einen einfachen Klasse - Wrapper für das Windows - CRITICAL_SECTION Objekt , so dass wir den Compiler automatisch initialisiert haben können , CRITICAL_SECTIONbevor main()aufgerufen. Im Idealfall wird eine echte RAII-Klasse für kritische Abschnitte verwendet, die Ausnahmen behandeln kann, die auftreten können, wenn der kritische Abschnitt gehalten wird. Dies würde jedoch den Rahmen dieser Antwort sprengen.

Die grundlegende Operation besteht darin, dass, wenn eine Instanz von Singletonangefordert wird, eine Sperre aufgehoben wird, der Singleton erstellt wird, wenn dies erforderlich ist, die Sperre aufgehoben und die Singleton-Referenz zurückgegeben wird.

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

Mann - das ist viel Mist, um "eine bessere Welt zu schaffen".

Die Hauptnachteile dieser Implementierung (wenn ich nicht einige Fehler durchgelassen habe) sind:

  • Wenn new Singleton()geworfen wird, wird das Schloss nicht freigegeben. Dies kann behoben werden, indem ein echtes RAII-Sperrobjekt anstelle des einfachen verwendet wird, das ich hier habe. Dies kann auch dazu beitragen, Dinge portabel zu machen, wenn Sie etwas wie Boost verwenden, um einen plattformunabhängigen Wrapper für das Schloss bereitzustellen.
  • Dies garantiert die Thread-Sicherheit, wenn die Singleton-Instanz nach dem main()Aufruf angefordert wird. Wenn Sie sie vorher aufrufen (wie bei der Initialisierung eines statischen Objekts), funktionieren die Dinge möglicherweise nicht, da die CRITICAL_SECTIONmöglicherweise nicht initialisiert wird.
  • Jedes Mal, wenn eine Instanz angefordert wird, muss eine Sperre vorgenommen werden. Wie gesagt, dies ist eine einfache thread-sichere Implementierung. Wenn Sie eine bessere benötigen (oder wissen möchten, warum Dinge wie die Double-Check-Lock-Technik fehlerhaft sind), lesen Sie die in Groos Antwort verlinkten Papiere .
Michael Burr
quelle
1
Oh oh. Was passiert bei new Singleton()Würfen?
sbi
@ Bob - um fair zu sein, mit einem richtigen Satz von Bibliotheken würde alles, was mit Nicht-Kopierbarkeit und einer richtigen RAII-Sperre zu tun hat, verschwinden oder minimal sein. Aber ich wollte, dass das Beispiel einigermaßen in sich geschlossen ist. Obwohl Singletons eine Menge Arbeit für vielleicht minimalen Gewinn sind, habe ich sie nützlich gefunden, um die Verwendung von Globals zu verwalten. Sie machen es in der Regel einfacher herauszufinden, wo und wann sie verwendet werden, etwas besser als nur eine Namenskonvention.
Michael Burr
@sbi: In diesem Beispiel gibt es bei new Singleton()Würfen definitiv ein Problem mit der Sperre. Es sollte eine geeignete RAII-Sperrklasse verwendet werden, etwa lock_guardvon Boost. Ich wollte, dass das Beispiel mehr oder weniger in sich geschlossen ist, und es war schon ein bisschen wie ein Monster, also habe ich die Ausnahmesicherheit weggelassen (aber es herausgerufen). Vielleicht sollte ich das beheben, damit dieser Code nicht an einer unangemessenen Stelle ausgeschnitten und eingefügt wird.
Michael Burr
Warum den Singleton dynamisch zuweisen? Warum nicht einfach 'pInstance' zu einem statischen Mitglied von 'Singleton :: instance ()' machen?
Martin York
@ Martin - fertig. Sie haben Recht, das macht es ein bisschen einfacher - wäre sogar noch besser, wenn ich eine RAII-Sperrklasse verwenden würde.
Michael Burr
10

Mit Blick auf den nächsten Standard (Abschnitt 6.7.4) wird erläutert, wie die statische lokale Initialisierung threadsicher ist. Sobald dieser Standardabschnitt weit verbreitet ist, wird Meyers Singleton die bevorzugte Implementierung sein.

Ich bin mit vielen Antworten bereits nicht einverstanden. Die meisten Compiler implementieren die statische Initialisierung bereits auf diese Weise. Die einzige bemerkenswerte Ausnahme ist Microsoft Visual Studio.

deft_code
quelle
6

Die richtige Antwort hängt von Ihrem Compiler ab. Es kann entscheiden , es threadsicher zu machen ; es ist nicht "natürlich" threadsicher.

MSalters
quelle
5

Ist der folgende [...] Implementierungsthread sicher?

Auf den meisten Plattformen ist dies nicht threadsicher. (Fügen Sie den üblichen Haftungsausschluss hinzu, in dem erklärt wird, dass der C ++ - Standard nichts über Threads weiß. Rechtlich sagt er also nicht aus, ob dies der Fall ist oder nicht.)

Wenn nicht, warum [...]?

Der Grund dafür ist nicht, dass nichts mehr als einen Thread daran hindert s, den Konstruktor gleichzeitig auszuführen .

Wie macht man es threadsicher?

"C ++ und die Gefahren des Double-Checked Locking" von Scott Meyers und Andrei Alexandrescu ist eine ziemlich gute Abhandlung zum Thema fadensicherer Singletons.

sbi
quelle
2

Wie MSalters sagte: Dies hängt von der von Ihnen verwendeten C ++ - Implementierung ab. Überprüfen Sie die Dokumentation. Was die andere Frage betrifft: "Wenn nicht, warum?" - Der C ++ - Standard erwähnt noch nichts über Threads. Die kommende C ++ - Version kennt jedoch Threads und gibt ausdrücklich an, dass die Initialisierung statischer lokaler Threads threadsicher ist. Wenn zwei Threads eine solche Funktion aufrufen, führt ein Thread eine Initialisierung durch, während der andere blockiert und auf den Abschluss wartet.

sellibitze
quelle