Const significa thread-safe in C ++ 11?


116

Ho sentito che constsignifica thread-safe in C ++ 11 . È vero?

Questo significa che constè ora l'equivalente di Java s' synchronized?

Stanno esaurendo le parole chiave ?


1
Le domande frequenti su C ++ sono generalmente gestite dalla comunità C ++ e potresti gentilmente venire a chiederci opinioni nella nostra chat.
Cucciolo

@DeadMG: non ero a conoscenza del C ++ - faq e della sua etichetta, è stato suggerito in un commento.
K-ballo

2
Dove hai sentito che const significa thread-safe?
Mark B

2
@Mark B: Herb Sutter e Bjarne Stroustrup lo dicevano alla Standard C ++ Foundation , vedi il link in fondo alla risposta.
K-ballo

NOTA PER CHI VIENE QUI: la vera domanda NON è se const significhi thread-safe. Sarebbe una sciocchezza, poiché altrimenti significherebbe che dovresti essere in grado di andare avanti e contrassegnare ogni metodo thread-safe come const. Piuttosto, la domanda che ci stiamo veramente ponendo è const IMPLIES thread-safe, ed è di questo che tratta questa discussione.
user541686

Risposte:


132

Ho sentito che constsignifica 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 :

  1. Ci sono due o più azioni eseguite contemporaneamente su una determinata cosa; e
  2. 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 constoggetti siano thread-safe . Ciò significa che la libreria standard non introdurrà una gara di dati fintanto che anche le operazioni su constoggetti del proprio tipo

  1. Consiste interamente di letture, cioè non ci sono scritture; o
  2. 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, constsignifica 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 constsia 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 areada tutti i thread che desideri e otterrai sempre risultati corretti.

Nota che questo non significa che rectsia thread-safe . In effetti, è facile vedere come se una chiamata a areadovesse avvenire nello stesso momento in cui una chiamata a set_sizeun dato rect, areapotrebbe finire per calcolare il suo risultato in base a una vecchia larghezza e una nuova altezza (o anche a valori incomprensibili) .

Ma va bene, rectnon è constcosì 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_castqualcosa 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 intcon 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 areapuò 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 rectin un contenitore standard, direttamente o indirettamente, stiamo stipulando un contratto con la Standard Library . Per continuare a scrivere in una constfunzione 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 areafunzione thread-safe , ma rectancora non è thread-safe . Una chiamata a areaaccadere nello stesso momento in cui una chiamata a set_sizepotrebbe comunque finire per calcolare il valore sbagliato, poiché le assegnazioni a widthe heightnon 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 constemutable - Herb Sutter


6
@ Ben Voigt: Mi risulta che la specifica C ++ 11 per std::stringsia formulata in un modo che già vieta COW . Non ricordo i dettagli, però ...
K-ballo

3
@ BenVoigt: No. Impedirebbe semplicemente che tali cose non siano sincronizzate, cioè non thread-safe. C ++ 11 vieta già esplicitamente COW - questo particolare passaggio non ha nulla a che fare con questo, però, e non vieta COW.
Cucciolo

2
Mi sembra che ci sia una lacuna logica. [17.6.5.9/3] vieta "troppo" dicendo "non deve modificare direttamente o indirettamente"; si dovrebbe dire "non sono direttamente o indirettamente, di introdurre una gara di dati", a meno che una scrittura atomica è un posto definito non essere una "modifica". Ma non riesco a trovarlo da nessuna parte.
Andy Prowl

1
Probabilmente ho reso il mio punto un po 'più chiaro qui: isocpp.org/blog/2012/12/… Grazie per aver cercato di aiutare comunque.
Andy Prowl

1
a volte mi chiedo chi fosse quello (o quelli direttamente coinvolti) effettivamente responsabile della stesura di alcuni paragrafi standard come questi.
pepper_chico
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.