Quindi ho visto un sacco di articoli che sostengono che in C++ il doppio controllo bloccato, comunemente usato per impedire a più thread di provare a inizializzare un singleton creato pigramente, è rotto. doppio codice di blocco normale controllato legge come questo:Cosa c'è di sbagliato in questa soluzione per il blocco a doppio controllo?
class singleton {
private:
singleton(); // private constructor so users must call instance()
static boost::mutex _init_mutex;
public:
static singleton & instance()
{
static singleton* instance;
if(!instance)
{
boost::mutex::scoped_lock lock(_init_mutex);
if(!instance)
instance = new singleton;
}
return *instance;
}
};
Il problema apparentemente rappresenta l'assegnazione dell'istanza linea - il compilatore è libero di allocare l'oggetto e quindi assegnare il puntatore ad esso, o per impostare il puntatore a dove sarà assegnato, quindi assegnarlo. L'ultimo caso rompe l'idioma - un thread può allocare la memoria e assegnare il puntatore ma non eseguire il costruttore del singleton prima che venga messo in stop - quindi il secondo thread vedrà che l'istanza non è nullo e prova a restituirlo , anche se non è stato ancora costruito.
I saw a suggestion per utilizzare un booleano locale del thread e controllare che invece di instance
. Qualcosa di simile:
class singleton {
private:
singleton(); // private constructor so users must call instance()
static boost::mutex _init_mutex;
static boost::thread_specific_ptr<int> _sync_check;
public:
static singleton & instance()
{
static singleton* instance;
if(!_sync_check.get())
{
boost::mutex::scoped_lock lock(_init_mutex);
if(!instance)
instance = new singleton;
// Any non-null value would work, we're really just using it as a
// thread specific bool.
_sync_check = reinterpret_cast<int*>(1);
}
return *instance;
}
};
questo modo ogni filo finisce controllare se l'istanza è stata creata una volta, ma si ferma dopo che, che comporta qualche calo di prestazioni ma non così male come bloccaggio ogni chiamata. Ma cosa succede se abbiamo appena usato un bool statico locale ?:
class singleton {
private:
singleton(); // private constructor so users must call instance()
static boost::mutex _init_mutex;
public:
static singleton & instance()
{
static bool sync_check = false;
static singleton* instance;
if(!sync_check)
{
boost::mutex::scoped_lock lock(_init_mutex);
if(!instance)
instance = new singleton;
sync_check = true;
}
return *instance;
}
};
Perché questo non funziona? Anche se sync_check dovesse essere letto da un thread quando viene assegnato in un altro, il valore spazzatura sarà ancora diverso da zero e quindi vero. This Dr. Dobb's article afferma che devi bloccare perché non vincerai mai una battaglia con il compilatore su istruzioni di riordino. Il che mi fa pensare che questo non debba funzionare per qualche motivo, ma non riesco a capire perché. Se i requisiti sui punti di sequenza sono perduti come l'articolo del Dr. Dobb mi fa credere, non capisco perché qualsiasi codice dopo il blocco non può essere riordinato per essere prima del blocco. Quale renderebbe il periodo di interruzione multithreading di C++.
Immagino che potrei vedere il compilatore essere autorizzato a riordinare in modo specifico sync_check prima del blocco perché è una variabile locale (e anche se è statico non stiamo restituendo un riferimento o un puntatore ad esso) - ma poi questo potrebbe ancora essere risolto rendendolo un membro statico (effettivamente globale).
Così funzionerà o no? Perché?
Il problema è che la variabile può essere assegnata prima che il costruttore sia eseguito (o completato), non prima che l'oggetto sia allocato. – kdgregory
Grazie, corretto. Ho completamente erroneamente ricordato le condizioni della gara. –
Sì, sei corretto, l'attuale C++ è davvero "periodo di interruzione del multithreading". quando si considera solo lo standard. I produttori di compilatori solitamente forniscono soluzioni per questo, quindi i risultati pratici non sono così terribili. – Suma