2013-02-14 16 views
8

Ho appena visto il discorso di Herb Sutter: C++ and Beyond 2012: Herb Sutter - atomic<> Weapons, 2 of 2Confusione su errore di implementazione all'interno shared_ptr distruttore

Egli mostra bug nella realizzazione di std :: shared_ptr distruttore:

if(control_block_ptr->refs.fetch_sub(1, memory_order_relaxed) == 0) 
    delete control_block_ptr; // B 

Egli dice, che a causa di memory_order_relaxed, delete può essere inserito prima di fetch_sub.

Al 01:25:18 - uscita non mantiene la linea B di sotto, dove dovrebbe essere

come ciò sia possibile? C'è una relazione accade-prima/sequenza-prima, perché sono entrambi in thread singolo. Potrei sbagliarmi, ma c'è anche carry-a-dependency tra fetch_sub ed delete.

Se ha ragione, quali elementi ISO lo supportano?

risposta

0

Nella conversazione Herb mostra memory_order_release non memory_order_relaxed, ma rilassato avrebbe ancora più problemi.

A meno che delete control_block_ptr non acceda a control_block_ptr->refs (cosa che probabilmente non lo è), l'operazione atomica non porta-a-dipendenza-all'eliminazione. L'operazione di cancellazione potrebbe non toccare alcuna memoria nel blocco di controllo, potrebbe solo restituire quel puntatore all'allocatore del freestore.

Ma non sono sicuro che Herb stia parlando del compilatore che sposta l'eliminazione prima dell'operazione atomica, o semplicemente si riferisce a quando gli effetti collaterali diventano visibili ad altri thread.

+0

"parlando del compilatore che sposta l'eliminazione prima dell'operazione atomica" - 1:23:34: "il codice rimane sotto e sopra" ;;; "o semplicemente riferendosi a quando gli effetti collaterali diventano visibili ad altri thread." - quali effetti collaterali? leggi-modifica-scrivi ogni volta vedi l'ultimo valore nell'ordine di modifica – qble

+0

"ma rilassato avrebbe ancora più problemi." - quali problemi? – qble

+0

_ "quali problemi?" _ Un'operazione rilassata non è affatto un'operazione di sincronizzazione. –

0

Sembra che stia parlando della sincronizzazione delle azioni sull'oggetto condiviso stesso, che non sono mostrate sui suoi blocchi di codice (e come risultato - confuse).

Ecco perché ha inserito acq_rel - perché tutte le azioni sull'oggetto devono avvenire prima della sua distruzione, tutto in ordine.

Ma non sono ancora sicuro del motivo per cui parla di scambio delete con fetch_sub.

+0

Sì, il motivo principale per cui è necessaria un'operazione di acquisizione-acquisizione è per la sincronizzazione inter-thread, egli menziona solo il movimento del codice nel passaggio e non lo spiega –

+0

"necessario è per la sincronizzazione tra thread" - è necessario per il codice non mostrato nella diapositiva ... – qble

+0

Sì, solo una parte di essa è mostrata: il control block potrebbe contenere un deleter personalizzato e il deleter potrebbe essere richiamato in un altro thread, quindi il blocco di controllo non deve essere distrutto finché il deleter non ha finito di distruggere l'oggetto. –

2

Immaginate un codice che rilascia un puntatore comune:

auto tmp = &(the_ptr->a); 
*tmp = 10; 
the_ptr.dec_ref(); 

Se dec_ref() non dispone di una "release" semantica, è perfettamente soddisfacente per un compilatore (o CPU) per spostare le cose da prima dec_ref() per dopo (per esempio):

auto tmp = &(the_ptr->a); 
the_ptr.dec_ref(); 
*tmp = 10; 

e questo non è sicuro, poiché dec_ref() può anche essere chiamato da altro thread nello stesso tempo ed eliminare l'oggetto. Quindi, deve avere una semantica di "rilascio" per le cose prima di dec_ref() per rimanere lì.

Ora lascia immaginare distruttore di quell'oggetto assomiglia a questo:

~object() { 
    auto xxx = a; 
    printf("%i\n", xxx); 
} 

anche modificheremo esempio un po 'e avrà 2 discussioni:

// thread 1 
auto tmp = &(the_ptr->a); 
*tmp = 10; 
the_ptr.dec_ref(); 

// thread 2 
the_ptr.dec_ref(); 

Poi, il codice "aggregato" cercherà come:

// thread 1 
auto tmp = &(the_ptr->a); 
*tmp = 10; 
{ // the_ptr.dec_ref(); 
    if (0 == atomic_sub(...)) { 
     { //~object() 
      auto xxx = a; 
      printf("%i\n", xxx); 
     } 
    } 
} 

// thread 2 
{ // the_ptr.dec_ref(); 
    if (0 == atomic_sub(...)) { 
     { //~object() 
      auto xxx = a; 
      printf("%i\n", xxx); 
     } 
    } 
} 

Tuttavia, se abbiamo solo un "rilascio" semantica per atomic_sub(), questo c ode può essere ottimizzato in questo modo:

// thread 2 
auto xxx = the_ptr->a; // "auto xxx = a;" from destructor moved here 
{ // the_ptr.dec_ref(); 
    if (0 == atomic_sub(...)) { 
     { //~object() 
      printf("%i\n", xxx); 
     } 
    } 
} 

Ma in questo modo, non sarà sempre distruttore stampare l'ultimo valore di "a" (questo codice non è la gara più libero). Ecco perché abbiamo anche bisogno di acquisire semantica per atomic_sub (o, in senso stretto, abbiamo bisogno di una barriera di acquisizione quando il contatore diventa 0 dopo il decremento).

+0

Molto bello esempio. Questo è solo un problema per gli oggetti con i dvd non banali che devono lavorare con lo stato mutabile dell'oggetto, giusto? Quindi un atomico 'rilassato 'è totalmente sicuro riguardo all'effetto collaterale di cancellazione della cancellazione di' delete'? – tmyklebu

+0

In altre parole, la semantica "release" è sufficiente per gli oggetti con distruttori banali. "delete" stesso ovviamente non può essere riordinato come una intera operazione - solo le parti "read" possono essere spostate "su" fuori da esso. le parti di "scrittura" non possono essere spostate "su" perché le scritture speculative non sono consentite e la condizione "se" deve essere prima verificata. –

+0

Ho capito che il rilascio è sufficiente per i trivial dtors, ma è anche abbastanza rilassato? – tmyklebu

0

Questa è una risposta tardiva.

Cominciamo con questo semplice tipo:

struct foo 
{ 
    ~foo() { std::cout << value; } 
    int value; 
}; 

E useremo questo tipo in un shared_ptr, come segue:

void runs_in_separate_thread(std::shared_ptr<foo> my_ptr) 
{ 
    my_ptr->value = 5; 
    my_ptr.reset(); 
} 

int main() 
{ 
    std::shared_ptr<foo> my_ptr(new foo); 
    std::async(std::launch::async, runs_in_separate_thread, my_ptr); 
    my_ptr.reset(); 
} 

Due le discussioni saranno in esecuzione in parallelo, sia la condivisione proprietà di un oggetto foo.

Con un'implementazione corretta shared_ptr (ovvero uno con memory_order_acq_rel), questo programma ha un comportamento definito. L'unico valore che questo programma stamperà è 5.

Con un'implementazione errata (utilizzando memory_order_relaxed) ci sono tali garanzie. Il comportamento non è definito poiché viene introdotta una corsa dati di foo::value. Il problema si verifica solo nei casi in cui il distruttore viene chiamato nel thread principale. Con un ordine di memoria rilassato, la scrittura da a foo::value nell'altro thread non può propagarsi al distruttore nel thread principale. È possibile stampare un valore diverso da 5.

Quindi cos'è una corsa di dati? Beh, controlla la definizione e prestare attenzione alla ultimo punto:

Quando una valutazione di un'espressione scrive in una posizione di memoria e un altro di valutazione legge o modifica la stessa posizione di memoria, le espressioni si dice che il conflitto.Un programma che ha due valutazioni contrastanti ha una corsa di dati a meno che non sia

  • entrambe le valutazioni contrastanti sono operazioni atomiche (vedi std :: atomica)
  • una delle valutazioni contrastanti accade-prima di un altro (vedi std :: memory_order)

Nel nostro programma, un thread scriverà al foo::value e un thread sarà leggere foo::value. Questi dovrebbero essere sequenziali; scrivere a foo::value dovrebbe sempre accadere prima della lettura. Intuitivamente, ha senso che sarebbero come il distruttore dovrebbe essere l'ultima cosa che accade a un oggetto.

memory_order_relaxed non offre tuttavia tali garanzie di ordinazione e pertanto è necessario memory_order_acq_rel.