5

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é?

+2

Il problema è che la variabile può essere assegnata prima che il costruttore sia eseguito (o completato), non prima che l'oggetto sia allocato. – kdgregory

+0

Grazie, corretto. Ho completamente erroneamente ricordato le condizioni della gara. –

+1

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

risposta

5

La correzione non risolve nulla poiché le scritture su sync_check e l'istanza possono essere eseguite in ordine sulla CPU. Ad esempio, immagina che le prime due chiamate all'istanza si verifichino all'incirca nello stesso momento su due diverse CPU. Il primo thread acquisirà il lock, inizializzerà il puntatore e imposterà sync_check su true, in quell'ordine, ma il processore potrebbe cambiare l'ordine delle scritture in memoria. Sull'altra CPU, quindi, è possibile che il secondo thread controlli sync_check, vedere che è vero, ma l'istanza potrebbe non essere ancora scritta in memoria. Vedi Lockless Programming Considerations for Xbox 360 and Microsoft Windows per i dettagli.

La soluzione specifica per il thread sync_check che hai menzionato dovrebbe funzionare (supponendo di inizializzare il puntatore su 0).

+0

Per quanto riguarda l'ultima frase: Sì, ma non sono sicuro, ma penso che thread_specific_ptr usi internamente un mutex. Quindi quale sarebbe il punto di usare quella soluzione contro il solo blocco del mutex (nessun doppio blocco)? – n1ckp

1

C'è qualche grande lettura di questo (anche se è .net/C# oriented) qui: http://msdn.microsoft.com/en-us/magazine/cc163715.aspx

Che cosa si riduce a è che è necessario essere in grado di dire la CPU che non è possibile riordinare la tua legge/scrive per questo accesso variabile (dal momento che il Pentium originale, la CPU può riordinare certe istruzioni se pensa che la logica non sia influenzata), e che deve assicurarsi che la cache sia coerente (non dimenticarlo - noi arrivare a far finta che tutta la memoria sia solo una risorsa piatta, ma in realtà ogni core CPU ha cache, alcuni non condivisi (L1), alcuni potrebbero essere condivisi a volte (L2)) - l'inizializzazione potrebbe scrivere nella RAM principale, ma un altro core potrebbe avere il valore non inizializzato nella cache. Se non si dispone di semantica della concorrenza, la CPU potrebbe non sapere che la cache è sporca.

Non conosco il lato C++, ma in .net, si dovrebbe designare la variabile come volatile per proteggere l'accesso ad esso (o si useranno i metodi di lettura/scrittura della memoria in System.Threading).

Per inciso, ho letto che in .net 2.0, doppia chiusura controllate è garantito il funzionamento senza variabili "volatili" (per tutti i lettori .net là fuori) - che non ti aiuta con il vostro C++ codice.

Se si desidera essere sicuri, è necessario eseguire l'equivalente C++ di contrassegnare una variabile come volatile in C#.

+1

Le variabili C++ possono essere dichiarate come volatili, ma dubito che abbia esattamente la stessa semantica di C#. Ricordo anche di aver letto da qualche parte che si trattava di un abuso di volatilità, ma non ricordo perché, quindi, non posso giudicare quanto fosse ragionato l'articolo. –

+0

In diverse lingue, potrebbe essere un abuso (potrebbe anche essere un abuso in C#). Uno degli aspetti veramente difficili della scrittura di codice a basso o basso blocco è stata la disparità nella guida. Ho passato del tempo a leggerlo e sembra che persino all'interno di Microsoft alcuni blogger sembrano contraddirsi a vicenda quando hai bisogno di una recinzione di memoria e quando devi usare volatile. È un problema difficile, certo. – JMarsch

+0

Non esiste alcun equivalente di .NET volatile nell'attuale C++ (come definito dallo standard). È una delle aree in arrivo che porterà lo standard C++ 0x. Nel frattempo è necessario utilizzare ciò che offre il compilatore (che in Visual Studio significa volatile e recinzione di memoria). – Suma

0

"L'ultimo caso rompe l'idioma - due thread potrebbero finire per creare il singleton."

Ma se capisco correttamente il codice, il primo esempio, si controlla se l'istanza esiste già (potrebbe essere eseguita da più thread contemporaneamente), se non è un thread get a bloccarlo e crea il istanza: solo un thread può eseguire la creazione in quel momento. Tutti gli altri thread vengono bloccati e attenderanno.

Una volta che l'istanza è stata creata e il mutex è sbloccato, il successivo thread in attesa bloccherà il mutex ma non tenterà di creare una nuova istanza perché il controllo fallirà.

La prossima volta che la variabile di istanza è selezionata verrà impostata in modo che nessun thread proverà a creare una nuova istanza.

Non sono sicuro del caso in cui un thread assegna un nuovo puntatore di istanza all'istanza mentre un altro thread controlla la stessa variabile, ma credo che verrà gestito correttamente in questo caso.

Mi manca qualcosa qui?

Ok, non sono sicuro del riordino delle operazioni, ma in questo caso cambierebbe la logica, quindi non mi aspetto che accada, ma non sono esperto in questo argomento.

+0

Hai ragione - mi sbagliavo riguardo alle condizioni di gara reali. Il problema è che un secondo thread potrebbe vedere l'istanza non è nulla e provare a restituirla prima che sia stata creata dal primo thread. Ho modificato il mio post. –

Problemi correlati