L'implementazione di Meyers del thread del pattern Singleton è sicura?


145

La seguente implementazione, usando l'inizializzazione lazy, del Singletonthread (Meyers 'Singleton) è sicura?

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

In caso contrario, perché e come renderlo sicuro?


Qualcuno può spiegare perché questo non è thread-safe. Gli articoli citati nei collegamenti discutono della sicurezza dei thread usando un'implementazione alternativa (usando una variabile puntatore, ovvero Singleton * pInstance).
Ankur,



Risposte:


168

In C ++ 11 , è thread-safe. Secondo la norma , §6.7 [stmt.dcl] p4:

Se il controllo inserisce la dichiarazione contemporaneamente mentre la variabile viene inizializzata, l' esecuzione simultanea deve attendere il completamento dell'inizializzazione.

Il supporto GCC e VS per la funzionalità ( inizializzazione dinamica e distruzione con concorrenza , noto anche come Magic Statics su MSDN ) è il seguente:

Grazie a @Mankarse e @olen_gam per i loro commenti.


In C ++ 03 , questo codice non era thread-safe. C'è un articolo di Meyers chiamato "C ++ and the Perils of Double-Checked Locking" che discute le implementazioni thread-safe del pattern e la conclusione è, più o meno, che (in C ++ 03) il blocco completo attorno al metodo di istanziazione è fondamentalmente il modo più semplice per garantire una corretta concorrenza su tutte le piattaforme, mentre la maggior parte delle forme di varianti di schemi di blocco con doppio controllo possono soffrire di condizioni di competizione su determinate architetture , a meno che le istruzioni non siano intercalate con barriere di memoria posizionate strategicamente.


3
C'è anche un'ampia discussione sul Singleton Pattern (durata e sicurezza del filo) di Alexandrescu in Modern C ++ Design. Vedi il sito di Loki: loki-lib.sourceforge.net/index.php?n=Pattern.Singleton
Matthieu M.

1
È possibile creare un singleton thread-safe con boost :: call_once.
CashCow,

1
Sfortunatamente, questa parte dello standard non è implementata nel compilatore C ++ di Visual Studio 2012. Descritta come "Magic statica" nella cartella "C ++ 11 Core Lingua Caratteristiche: Concurrency" tavolo qui: msdn.microsoft.com/en-us/library/vstudio/hh567368.aspx
olen_garn

Lo snippet dello standard riguarda la costruzione ma non la distruzione. Lo standard impedisce che l'oggetto venga distrutto su un thread mentre (o prima) un altro thread tenta di accedervi al termine del programma?
Stewbasic,

IANA (linguaggio C ++) L ma la sezione 3.6.3 [basic.start.term] p2 suggerisce che è possibile colpire un comportamento indefinito tentando di accedere all'oggetto dopo che è stato distrutto?
Stewbasic,

21

Per rispondere alla tua domanda sul perché non è thread-safe, non è perché la prima chiamata a cui instance()deve chiamare il costruttore Singleton s. Per essere thread-safe questo dovrebbe avvenire in una sezione critica, ma non c'è alcun requisito nello standard che venga presa una sezione critica (lo standard fino ad ora è completamente silenzioso sui thread). I compilatori spesso lo implementano usando un semplice controllo e un incremento di un valore booleano statico, ma non in una sezione critica. Qualcosa come il seguente pseudocodice:

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

Quindi ecco un semplice Singleton thread-safe (per Windows). Utilizza un semplice wrapper di classe per l'oggetto CRITICAL_SECTION di Windows in modo che possiamo fare in modo che il compilatore inizializzi automaticamente il nome CRITICAL_SECTIONprecedente main(). Idealmente, verrebbe utilizzata una vera classe di sezione critica RAII in grado di gestire le eccezioni che potrebbero verificarsi quando si tiene la sezione critica, ma questo va oltre lo scopo di questa risposta.

L'operazione fondamentale è che quando Singletonviene richiesta un'istanza di , viene acquisito un blocco, il Singleton viene creato se necessario, quindi il blocco viene rilasciato e viene restituito il riferimento 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;
}

Amico, è un sacco di merda per "rendere un mondo migliore".

Gli svantaggi principali di questa implementazione (se non ho lasciato passare alcuni bug) è:

  • se new Singleton()viene lanciato, il blocco non verrà rilasciato. Questo può essere risolto usando un vero oggetto di blocco RAII invece di quello semplice che ho qui. Questo può anche aiutare a rendere le cose portatili se usi qualcosa come Boost per fornire un wrapper indipendente dalla piattaforma per il blocco.
  • ciò garantisce la sicurezza del thread quando main()viene chiamata l'istanza di Singleton dopo che è stata chiamata - se la si chiama prima (come nell'inizializzazione di un oggetto statico) le cose potrebbero non funzionare perché CRITICAL_SECTIONpotrebbero non essere inizializzate.
  • un blocco deve essere preso ogni volta che viene richiesta un'istanza. Come ho già detto, questa è una semplice implementazione thread-safe. Se ne hai bisogno di uno migliore (o vuoi sapere perché cose come la tecnica del blocco a doppio controllo sono difettose), vedi i documenti collegati nella risposta di Groo .

1
Uh Oh. Cosa succede se new Singleton()lancia?
sabato

@Bob - per essere onesti, con un set adeguato di librerie, tutta l'innesto che ha a che fare con la non copiabilità e un corretto blocco RAII andrebbe via o sarebbe minimo. Ma volevo che l'esempio fosse ragionevolmente autonomo. Anche se i singleton sono molto impegnativi per un guadagno forse minimo, li ho trovati utili nella gestione dell'uso dei globi. Tendono a rendere più facile scoprire dove e quando vengono utilizzati un po 'meglio di una semplice convenzione di denominazione.
Michael Burr,

@sbi: in questo esempio, se si new Singleton()lancia, c'è sicuramente un problema con il lucchetto. Dovrebbe essere utilizzata una classe di blocco RAII appropriata, qualcosa come lock_guardBoost. Volevo che l'esempio fosse più o meno autonomo, ed era già un po 'un mostro, quindi ho lasciato fuori la sicurezza delle eccezioni (ma l'ho chiamato fuori). Forse dovrei risolverlo in modo che questo codice non venga incollato da qualche parte in modo inappropriato.
Michael Burr,

Perché allocare dinamicamente il singleton? Perché non rendere 'pInstance' un membro statico di 'Singleton :: instance ()'?
Martin York,

@Martin - fatto. Hai ragione, questo rende un po 'più semplice - sarebbe ancora meglio se usassi una classe di blocco RAII.
Michael Burr,

10

Guardando allo standard successivo (sezione 6.7.4), viene spiegato come l'inizializzazione locale statica sia sicura da thread. Quindi, una volta che quella sezione di standard sarà ampiamente implementata, Meyer Singleton sarà l'implementazione preferita.

Non sono d'accordo con molte risposte già. La maggior parte dei compilatori implementa già l'inizializzazione statica in questo modo. L'unica eccezione notevole è Microsoft Visual Studio.


6

La risposta corretta dipende dal tuo compilatore. Può decidere di renderlo sicuro per i thread; non è un thread "sicuro".


5

Il seguente thread di implementazione [...] è sicuro?

Sulla maggior parte delle piattaforme, questo non è thread-safe. (Aggiungi il solito disclaimer che spiega che lo standard C ++ non conosce i thread, quindi, legalmente, non dice se lo sia o meno.)

In caso contrario, perché [...]?

La ragione per cui non lo è è che nulla impedisce a più di un thread di eseguire contemporaneamente s"costruttore.

come renderlo thread sicuro?

"C ++ and the Perils of Double-Checked Locking" di Scott Meyers e Andrei Alexandrescu è un ottimo trattato in materia di singoli thread-safe.


2

Come diceva MSalters: dipende dall'implementazione C ++ che usi. Controlla la documentazione. Per quanto riguarda l'altra domanda: "Se no, perché?" - Lo standard C ++ non menziona ancora nulla sui thread. Ma la prossima versione C ++ è a conoscenza dei thread e afferma esplicitamente che l'inizializzazione dei locali statici è thread-safe. Se due thread chiamano tale funzione, un thread eseguirà un'inizializzazione mentre l'altro bloccherà e attenderà che finisca.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.