Effizientes thread-sicheres Singleton in C ++


75

Das übliche Muster für eine Singleton-Klasse ist so etwas wie

static Foo &getInst()
{
  static Foo *inst = NULL;
  if(inst == NULL)
    inst = new Foo(...);
  return *inst;    
}

Nach meinem Verständnis ist diese Lösung jedoch nicht threadsicher, da 1) der Konstruktor von Foo möglicherweise mehrmals aufgerufen wird (was möglicherweise von Bedeutung ist oder nicht) und 2) inst möglicherweise nicht vollständig erstellt wird, bevor er an einen anderen Thread zurückgegeben wird .

Eine Lösung besteht darin, einen Mutex um die gesamte Methode zu wickeln, aber dann zahle ich für den Synchronisationsaufwand, lange nachdem ich ihn tatsächlich benötige. Eine Alternative ist so etwas wie

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

Ist dies der richtige Weg, oder gibt es Fallstricke, die ich beachten sollte? Gibt es beispielsweise Probleme mit der statischen Initialisierungsreihenfolge, die auftreten können, dh ist inst immer garantiert NULL, wenn getInst zum ersten Mal aufgerufen wird?


6
Aber Sie haben keine Zeit, ein Beispiel zu finden und eine enge Abstimmung abzuhalten? Ich bin gerade frisch.
Bmargulies

1
Mögliches Duplikat von stackoverflow.com/questions/6915/…
kennytm

3
@bmargulies Nein, der Fragesteller konnte sich offensichtlich nicht darum kümmern. Warum sollte ich das tun? Ich habe beschlossen, das Abstimmen und Schließen als Dupes aufzugeben, da ich einer der wenigen zu sein scheint, die sich die Mühe machen, Mist aus SO herauszuhalten. Und weißt du, Faulheit fühlt sich gut an!

Ich nahm mir Zeit, um mein Problem sorgfältig zu beschreiben, mit Ausschnitten und einer Diskussion darüber, was ich wusste / versucht hatte. Es tut mir leid, dass ich deine Zeit mit "Mist" verschwendet habe. :(
user168715

1
@sbi: Ich auch. Das Streuen von Antworten auf Tausende von Fragen ist der beste Weg, um es später schwierig zu machen, sie zu durchsuchen.
Matthieu M.

Antworten:


44

Ihre Lösung heißt "Double Checked Locking" und die Art und Weise, wie Sie sie geschrieben haben, ist nicht threadsicher.

Dieses Papier von Meyers / Alexandrescu erklärt, warum - aber dieses Papier wird auch weitgehend missverstanden. Es wurde das Mem "Double Checked Locking ist in C ++ unsicher" gestartet. Die tatsächliche Schlussfolgerung lautet jedoch, dass Double Checked Locking in C ++ sicher implementiert werden kann. Es erfordert lediglich die Verwendung von Speicherbarrieren an einer nicht offensichtlichen Stelle.

Das Dokument enthält einen Pseudocode, der zeigt, wie Speicherbarrieren zur sicheren Implementierung des DLCP verwendet werden. Daher sollte es für Sie nicht schwierig sein, Ihre Implementierung zu korrigieren.


if (inst == NULL) {temp = new Foo (...); inst = temp;} Garantiert das nicht, dass der Konstruktor fertig ist, bevor inst zugewiesen wurde? Mir ist klar, dass es weg optimiert werden kann (und wahrscheinlich wird), aber logischerweise löst das das Problem, nein?
stu

1
Das hilft nicht weiter, da ein kompatibler Compiler die Zuordnungs- und Konstruktionsschritte nach Belieben neu anordnen kann.
JoeG

Ich habe das Papier sorgfältig gelesen und es scheint, dass die Empfehlung einfach darin besteht, DLCP mit Singleton zu vermeiden. Sie müssen die Klasse verlassen und Speicherbarrieren hinzufügen (würde dies nicht auch die Effizienz beeinträchtigen?). Verwenden Sie für praktische Anforderungen eine einfache Einzelsperre und speichern Sie das von "GetInstance" erhaltene Objekt zwischen.
Guyarad

Lesen Sie auch einfach das Papier und ich verstand die Hauptschlussfolgerung als: DLCP kann threadsicher unter Verwendung von Speicherbarrieren implementiert werden, aber nicht auf tragbare Weise (vor c ++ 11)
idclev 463035818

97

Wenn Sie C ++ 11 verwenden, ist dies der richtige Weg:

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

Nach dem neuen Standard besteht keine Notwendigkeit mehr, sich um dieses Problem zu kümmern. Die Objektinitialisierung wird nur von einem Thread durchgeführt, andere Threads warten, bis sie abgeschlossen ist. Oder Sie können std :: call_once verwenden. (mehr Infos hier )


2
Dies ist die C ++ 11-Lösung, die von den Leuten erwartet wird.
Alexander Oh

8
Leider ist dies in VS2013 nicht threadsicher, siehe "Magic Statics" hier: msdn.microsoft.com/en-gb/library/hh567368.aspx
Chris Drew


8
Um Verwirrung zu vermeiden, können Sie der Funktionsdeklaration möglicherweise entweder statisch hinzufügen oder explizit angeben, dass es sich um eine Nichtmitgliedsfunktion handelt.
MikeMB

Sind Aufrufe für diese Instanzen von verschiedenen Threads threadsicher oder müssen Funktionen der instanziierten Klasse sich selbst um die Atomizität kümmern?
Shadasviar

12

Herb Sutter spricht über die doppelt überprüfte Verriegelung in der CppCon 2014.

Unten ist der Code, den ich basierend darauf in C ++ 11 implementiert habe:

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

Sie können das vollständige Programm auch hier überprüfen: http://ideone.com/olvK13


1
@Etherealone was ist dein Vorschlag?
Qqibrow

4
Eine einfache static Foo foo;und return &foo;in der Instanz - Funktion wäre genug; staticDie Initialisierung ist in C ++ 11 threadsicher. Verweisen Sie jedoch lieber auf Zeiger.
Etherealone

Ich erhalte die Fehlermeldung in MSVC 2015: Schweregrad Code Beschreibung Projektdatei Zeilenquelle Unterdrückungsstatus Fehler (aktiv) Mehr als ein Operator "==" stimmt mit diesen Operanden
überein

@qqibrow kann sein, dass Sie auch den Kopierkonstruktor, den Verschiebungskonstruktor, den Zuweisungsoperator und den Verschiebungszuweisungsoperator als privat festlegen können.
Mayur

9

Verwenden Sie pthread_oncediese Option , um sicherzustellen, dass die Initialisierungsfunktion einmal atomar ausgeführt wird.

(Unter Mac OS X wird eine Drehsperre verwendet. Sie kennen die Implementierung anderer Plattformen nicht.)


3

TTBOMK, die einzige garantierte thread-sichere Möglichkeit, dies ohne Sperren zu tun, besteht darin, alle Ihre Singletons zu initialisieren, bevor Sie jemals einen Thread starten.



0

Die ACE-Singleton-Implementierung verwendet ein doppelt geprüftes Sperrmuster für die Thread-Sicherheit. Sie können darauf verweisen, wenn Sie möchten.

Den Quellcode finden Sie hier .


0

Funktioniert TLS hier? https://en.wikipedia.org/wiki/Thread-local_storage#C_and_C++

Zum Beispiel,

static _thread Foo *inst = NULL;
static Foo &getInst()
{
  if(inst == NULL)
    inst = new Foo(...);
  return *inst;    
 }

Wir brauchen aber auch eine Möglichkeit, es explizit zu löschen, wie

static void deleteInst() {
   if (!inst) {
     return;
   }
   delete inst;
   inst = NULL;
}

-2

Die Lösung ist wegen der Anweisung nicht threadsicher

inst = new Foo();

kann vom Compiler in zwei Anweisungen unterteilt werden:

Anweisung1: inst = malloc (sizeof (Foo));
Anweisung2: inst-> Foo ();

Angenommen, nach Ausführung von Anweisung 1 durch einen Thread erfolgt ein Kontextwechsel. Und der 2. Thread führt auch die getInstance()Methode aus. Dann wird der 2. Thread feststellen, dass der 'inst'-Zeiger nicht null ist. Der zweite Thread gibt also einen Zeiger auf ein nicht initialisiertes Objekt zurück, da der Konstruktor vom ersten Thread noch nicht aufgerufen wurde.


Nein, es ist unsicher, Punkt. Es muss nicht "vom Compiler aufgelöst" werden, um unsicher zu sein.
Neugieriger
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.