Ho sentito che const
significa thread-safe in C ++ 11 . È vero?
In qualche modo è vero ...
Questo è ciò che il linguaggio standard ha da dire sulla sicurezza dei thread:
[1.10 / 4]
Due valutazioni di espressioni sono in conflitto se una di esse modifica una locazione di memoria (1.7) e l'altra accede o modifica la stessa locazione di memoria.
[1.10 / 21]
L'esecuzione di un programma contiene una corsa di dati se contiene due azioni in conflitto in thread differenti, almeno una delle quali non è atomica, e nessuna delle due avviene prima dell'altra. Qualsiasi tale competizione di dati si traduce in un comportamento indefinito.
che non è altro che la condizione sufficiente perché si verifichi una gara di dati :
- Ci sono due o più azioni eseguite contemporaneamente su una determinata cosa; e
- Almeno uno di loro è una scrittura.
La libreria standard si basa su questo, andando un po 'oltre:
[17.6.5.9/1]
Questa sezione specifica i requisiti che le implementazioni devono soddisfare per prevenire gare di dati (1.10). Ogni funzione di libreria standard deve soddisfare ogni requisito se non diversamente specificato. Le implementazioni possono impedire la corsa dei dati in casi diversi da quelli specificati di seguito.
[17.6.5.9/3]
Una funzione di libreria standard C ++ non deve modificare direttamente o indirettamente oggetti (1.10) accessibili da thread diversi dal thread corrente a meno che non si acceda agli oggetti direttamente o indirettamente tramite gliargomentinon const della funzione, inclusithis
.
che in parole semplici dice che si aspetta che le operazioni sugli const
oggetti siano thread-safe . Ciò significa che la libreria standard non introdurrà una gara di dati fintanto che anche le operazioni su const
oggetti del proprio tipo
- Consiste interamente di letture, cioè non ci sono scritture; o
- Sincronizza internamente le scritture.
Se questa aspettativa non vale per uno dei tuoi tipi, utilizzarlo direttamente o indirettamente insieme a qualsiasi componente della libreria standard potrebbe provocare una corsa di dati . In conclusione, const
significa thread-safe dal punto di vista della Standard Library . È importante notare che questo è semplicemente un contratto e non verrà applicato dal compilatore, se lo rompi ottieni un comportamento indefinito e sei da solo. Che const
sia presente o meno non influirà sulla generazione del codice, almeno non per quanto riguarda le gare di dati .
Questo significa che const
è ora l'equivalente di Java s' synchronized
?
No . Affatto...
Considera la seguente classe eccessivamente semplificata che rappresenta un rettangolo:
class rect {
int width = 0, height = 0;
public:
/*...*/
void set_size( int new_width, int new_height ) {
width = new_width;
height = new_height;
}
int area() const {
return width * height;
}
};
La funzione membro area
è thread-safe ; non perché è const
, ma perché consiste interamente in operazioni di lettura. Non ci sono scritture coinvolte e almeno una scrittura coinvolta è necessaria affinché si verifichi una gara di dati . Ciò significa che puoi chiamare area
da tutti i thread che desideri e otterrai sempre risultati corretti.
Nota che questo non significa che rect
sia thread-safe . In effetti, è facile vedere come se una chiamata a area
dovesse avvenire nello stesso momento in cui una chiamata a set_size
un dato rect
, area
potrebbe finire per calcolare il suo risultato in base a una vecchia larghezza e una nuova altezza (o anche a valori incomprensibili) .
Ma va bene, rect
non è const
così che non ci si aspetta nemmeno che sia thread-safe dopo tutto. Un oggetto dichiarato const rect
, d'altra parte, sarebbe thread-safe poiché non è possibile alcuna scrittura (e se stai considerando const_cast
qualcosa originariamente dichiarato const
, ottieni un comportamento indefinito e basta).
Allora cosa significa?
Supponiamo, per amor di discussione, che le operazioni di moltiplicazione siano estremamente costose ed è meglio evitarle quando possibile. Potremmo calcolare l'area solo se è richiesta, quindi memorizzarla nella cache nel caso in cui venga richiesta di nuovo in futuro:
class rect {
int width = 0, height = 0;
mutable int cached_area = 0;
mutable bool cached_area_valid = true;
public:
/*...*/
void set_size( int new_width, int new_height ) {
cached_area_valid = ( width == new_width && height == new_height );
width = new_width;
height = new_height;
}
int area() const {
if( !cached_area_valid ) {
cached_area = width;
cached_area *= height;
cached_area_valid = true;
}
return cached_area;
}
};
[Se questo esempio sembra troppo artificiale, potresti sostituirlo mentalmente int
con un numero intero allocato dinamicamente molto grande che è intrinsecamente non thread-safe e per il quale le moltiplicazioni sono estremamente costose.]
La funzione membro area
non è più thread-safe , sta eseguendo operazioni di scrittura e non è sincronizzata internamente. È un problema? La chiamata a area
può avvenire come parte di un costruttore di copia di un altro oggetto, tale costruttore potrebbe essere stato chiamato da qualche operazione su un contenitore standard , ea quel punto la libreria standard si aspetta che questa operazione si comporti come una lettura rispetto alle gare di dati . Ma stiamo scrivendo!
Non appena mettiamo un rect
in un contenitore standard, direttamente o indirettamente, stiamo stipulando un contratto con la Standard Library . Per continuare a scrivere in una const
funzione pur rispettando quel contratto, dobbiamo sincronizzare internamente tali scritture:
class rect {
int width = 0, height = 0;
mutable std::mutex cache_mutex;
mutable int cached_area = 0;
mutable bool cached_area_valid = true;
public:
/*...*/
void set_size( int new_width, int new_height ) {
if( new_width != width || new_height != height )
{
std::lock_guard< std::mutex > guard( cache_mutex );
cached_area_valid = false;
}
width = new_width;
height = new_height;
}
int area() const {
std::lock_guard< std::mutex > guard( cache_mutex );
if( !cached_area_valid ) {
cached_area = width;
cached_area *= height;
cached_area_valid = true;
}
return cached_area;
}
};
Nota che abbiamo reso la area
funzione thread-safe , ma rect
ancora non è thread-safe . Una chiamata a area
accadere nello stesso momento in cui una chiamata a set_size
potrebbe comunque finire per calcolare il valore sbagliato, poiché le assegnazioni a width
e height
non sono protette dal mutex.
Se volessimo davvero un thread-safe rect
, useremmo una primitiva di sincronizzazione per proteggere il non-thread-safe rect
.
Stanno esaurendo le parole chiave ?
Sì. Hanno esaurito le parole chiave sin dal primo giorno.
Fonte : non lo sai const
emutable
- Herb Sutter