Domanda

Esiste un modo per implementare un oggetto singleton in C++ che sia:

  1. Costruito pigramente in modo thread-safe (due thread potrebbero essere contemporaneamente il primo utente del singleton - dovrebbe comunque essere costruito solo una volta).
  2. Non si basa sulla costruzione anticipata di variabili statiche (quindi l'oggetto singleton è di per sé sicuro da utilizzare durante la costruzione di variabili statiche).

(Non conosco abbastanza bene il mio C++, ma è il caso che le variabili statiche integrali e costanti vengano inizializzate prima che venga eseguito qualsiasi codice (cioè, anche prima che i costruttori statici vengano eseguiti - i loro valori potrebbero già essere "inizializzati" nel programma Immagine)?Se è così, forse questo può essere sfruttato per implementare un mutex singleton, che a sua volta può essere utilizzato per proteggere la creazione del vero singleton..)


Eccellente, sembra che ora ho un paio di buone risposte (peccato non poter contrassegnare 2 o 3 come la risposta).Sembra che ci siano due soluzioni generali:

  1. Utilizza l'inizializzazione statica (in contrapposizione all'inizializzazione dinamica) di una variabile statica POD e implementa il mio mutex con quello utilizzando le istruzioni atomiche integrate.Questo era il tipo di soluzione a cui alludevo nella mia domanda e credo di saperlo già.
  2. Usa qualche altra funzione di libreria come pthread_once O boost::call_once.Questi certamente non li conoscevo e sono molto grato per le risposte pubblicate.
È stato utile?

Soluzione

Fondamentalmente, stai chiedendo la creazione sincronizzata di un singleton, senza utilizzare alcuna sincronizzazione (variabili precedentemente costruite).In generale no, questo non è possibile.Hai bisogno di qualcosa disponibile per la sincronizzazione.

Per quanto riguarda l'altra tua domanda, sì, le variabili statiche che possono essere inizializzate staticamente (ad es.nessun codice runtime necessario) sono garantiti per essere inizializzati prima che venga eseguito altro codice.Ciò rende possibile utilizzare un mutex inizializzato staticamente per sincronizzare la creazione del singleton.

Dalla revisione del 2003 dello standard C++:

Gli oggetti con durata di archiviazione statica (3.7.1) devono essere inizializzati a zero (8.5) prima che abbia luogo qualsiasi altra inizializzazione.L'inizializzazione zero e l'inizializzazione con un'espressione costante sono chiamate collettivamente inizializzazione statica;tutte le altre inizializzazioni sono inizializzazioni dinamiche.Gli oggetti di tipo POD (3.9) con durata di archiviazione statica inizializzata con espressioni costanti (5.19) devono essere inizializzati prima che abbia luogo qualsiasi inizializzazione dinamica.Gli oggetti con durata di archiviazione statica definita nell'ambito del namespace nella stessa unità di traduzione e inizializzati dinamicamente devono essere inizializzati nell'ordine in cui la loro definizione appare nell'unità di traduzione.

Se tu Sapere che utilizzerai questo singleton durante l'inizializzazione di altri oggetti statici, penso che scoprirai che la sincronizzazione non è un problema.Per quanto ne so, tutti i principali compilatori inizializzano oggetti statici in un singolo thread, quindi sicurezza del thread durante l'inizializzazione statica.Puoi dichiarare il tuo puntatore singleton come NULL e quindi controllare se è stato inizializzato prima di usarlo.

Tuttavia, ciò presuppone che tu Sapere che utilizzerai questo singleton durante l'inizializzazione statica.Anche questo non è garantito dallo standard, quindi se vuoi essere completamente sicuro, usa un mutex inizializzato staticamente.

Modificare:Il suggerimento di Chris di utilizzare un sistema di confronto e scambio atomico funzionerebbe sicuramente.Se la portabilità non è un problema (e la creazione di singleton temporanei aggiuntivi non è un problema), allora si tratta di una soluzione con un sovraccarico leggermente inferiore.

Altri suggerimenti

Sfortunatamente, la risposta di Matt presenta ciò che viene chiamato chiusura a doppio controllo che non è supportato dal modello di memoria C/C++.(È supportato dal modello di memoria Java 1.5 e versioni successive — e penso da .NET.) Ciò significa che tra il momento in cui pObj == NULL avviene il controllo e una volta acquisito il lock (mutex), pObj potrebbe essere già stato assegnato su un altro thread.Il cambio di thread avviene ogni volta che il sistema operativo lo desidera, non tra le "righe" di un programma (che non hanno significato dopo la compilazione nella maggior parte dei linguaggi).

Inoltre, come riconosce Matt, usa an int come un blocco piuttosto che come una primitiva del sistema operativo.Non farlo.I lock corretti richiedono l'uso di istruzioni di barriera della memoria, potenzialmente svuotamenti della linea della cache e così via;usa le primitive del tuo sistema operativo per il blocco.Ciò è particolarmente importante perché le primitive utilizzate possono cambiare tra le singole linee della CPU su cui gira il sistema operativo;ciò che funziona su una CPU Foo potrebbe non funzionare su CPU Foo2.La maggior parte dei sistemi operativi supporta nativamente i thread POSIX (pthreads) o li offre come wrapper per il pacchetto di threading del sistema operativo, quindi spesso è meglio illustrare esempi utilizzandoli.

Se il tuo sistema operativo offre primitive appropriate, e se ne hai assolutamente bisogno per le prestazioni, invece di fare questo tipo di blocco/inizializzazione puoi usare un confronto e scambio atomico operazione per inizializzare una variabile globale condivisa.In sostanza, ciò che scrivi sarà simile a questo:

MySingleton *MySingleton::GetSingleton() {
    if (pObj == NULL) {
        // create a temporary instance of the singleton
        MySingleton *temp = new MySingleton();
        if (OSAtomicCompareAndSwapPtrBarrier(NULL, temp, &pObj) == false) {
            // if the swap didn't take place, delete the temporary instance
            delete temp;
        }
    }

    return pObj;
}

Funziona solo se è sicuro creare più istanze del tuo singleton (una per thread che richiama GetSingleton() simultaneamente) e quindi eliminare gli extra.IL OSAtomicCompareAndSwapPtrBarrier funzione fornita su Mac OS X (la maggior parte dei sistemi operativi fornisce una primitiva simile) controlla se pObj È NULL e in realtà lo imposta solo su temp ad esso se lo è.Questo utilizza il supporto hardware per eseguire davvero, letteralmente, solo lo scambio una volta e dire se è successo.

Un'altra funzionalità da sfruttare se il tuo sistema operativo lo offre che si trova tra questi due estremi è pthread_once.Ciò ti consente di impostare una funzione che viene eseguita solo una volta, fondamentalmente eseguendo tutte le operazioni di blocco/barriera/ecc.inganno per te, non importa quante volte viene invocato o su quanti thread viene invocato.

Ecco un getter singleton molto semplice costruito pigramente:

Singleton *Singleton::self() {
    static Singleton instance;
    return &instance;
}

Questo è pigro e il successivo standard C++ (C++0x) richiede che sia thread-safe.In effetti, credo che almeno g++ lo implementi in modo thread-safe.Quindi, se questo è il tuo compilatore di destinazione O se usi un compilatore che lo implementa anche in modo thread-safe (forse i compilatori di Visual Studio più recenti lo fanno?Non lo so), allora questo potrebbe essere tutto ciò di cui hai bisogno.

Vedi anche http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2513.html su questo argomento.

Non puoi farlo senza variabili statiche, tuttavia se sei disposto a tollerarne una, puoi usarla Boost.Thread per questo scopo.Per ulteriori informazioni leggere la sezione "inizializzazione una tantum".

Quindi nella funzione di accesso singleton, utilizzare boost::call_once per costruire l'oggetto e restituirlo.

Per gcc, questo è piuttosto semplice:

LazyType* GetMyLazyGlobal() {
    static const LazyType* instance = new LazyType();
    return instance;
}

GCC si assicurerà che l'inizializzazione sia atomica. Per VC++, questo non è il caso. :-(

Uno dei principali problemi con questo meccanismo è la mancanza di testabilità:se è necessario reimpostare LazyType su uno nuovo tra un test o l'altro o desideri modificare LazyType* in MockLazyType*, non sarai in grado di farlo.Detto questo, di solito è meglio usare un mutex statico + puntatore statico.

Inoltre, eventualmente una digressione:È meglio evitare sempre i tipi statici non POD.(I puntatori ai POD vanno bene.) Le ragioni di ciò sono molte:come hai detto, l'ordine di inizializzazione non è definito, né lo è l'ordine in cui vengono chiamati i distruttori.Per questo motivo, i programmi finiranno per bloccarsi quando tentano di uscire;spesso non è un grosso problema, ma a volte è un ostacolo quando il profiler che stai tentando di utilizzare richiede un'uscita pulita.

Anche se a questa domanda è già stata data una risposta, penso che ci siano altri punti da menzionare:

  • Se desideri un'istanziazione pigra del singleton mentre usi un puntatore a un'istanza allocata dinamicamente, dovrai assicurarti di ripulirlo al punto giusto.
  • Potresti utilizzare la soluzione di Matt, ma dovresti utilizzare una sezione mutex/critica adeguata per il blocco e controllare "pObj == NULL" sia prima che dopo il blocco.Ovviamente, pObj dovrebbe anche esserlo statico ;).Un mutex sarebbe inutilmente pesante in questo caso, sarebbe meglio optare per una sezione critica.

Ma come già affermato, non è possibile garantire l'inizializzazione lenta thread-safe senza utilizzare almeno una primitiva di sincronizzazione.

Modificare:Sì, Derek, hai ragione.Colpa mia.:)

Potresti utilizzare la soluzione di Matt, ma dovresti utilizzare una sezione mutex/critica adeguata per il blocco e controllare "pObj == NULL" sia prima che dopo il blocco.Naturalmente, anche pObj dovrebbe essere statico ;) .Un mutex sarebbe inutilmente pesante in questo caso, sarebbe meglio optare per una sezione critica.

GU, non funziona.Come ha sottolineato Chris, si tratta di un doppio controllo del blocco, che non è garantito che funzioni nell'attuale standard C++.Vedere: C++ e i pericoli del blocco ricontrollato

Modificare:Nessun problema, GU.È davvero carino nelle lingue in cui funziona.Mi aspetto che funzionerà in C++0x (anche se non ne sono sicuro), perché è un linguaggio così conveniente.

  1. continua a leggere modello di memoria debole.Può rompere serrature e spinlock a doppio controllo.Intel è un modello di memoria potente (ancora), quindi su Intel è più semplice

  2. usa attentamente "volatile" per evitare di memorizzare nella cache parti dell'oggetto nei registri, altrimenti avrai inizializzato il puntatore dell'oggetto, ma non l'oggetto stesso, e l'altro thread andrà in crash

  3. l'ordine di inizializzazione delle variabili statiche rispetto al caricamento del codice condiviso a volte non è banale.Ho visto casi in cui il codice per distruggere un oggetto era già stato scaricato, quindi il programma si è bloccato all'uscita

  4. tali oggetti sono difficili da distruggere correttamente

In generale i singleton sono difficili da eseguire correttamente e difficili da eseguire il debug.È meglio evitarli del tutto.

Suppongo di dire di non farlo perché non è sicuro e probabilmente si romperà più spesso rispetto alla semplice inizializzazione di queste cose main() non sarà così popolare.

(E sì, lo so, suggerire ciò significa che non dovresti tentare di fare cose interessanti nei costruttori di oggetti globali.Questo è il punto.)

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top