2010-09-06 13 views
5

Ho un'applicazione C++ multi-thread.Parola chiave volatile C++ con variabile globale condivisa a cui si accede dalla funzione

Ora so che per le variabili globali condivise, si dovrebbe usare volatile in alcuni casi mentre si controlla lo stato della variabile altrimenti il ​​compilatore potrebbe assumere che il valore della variabile non cambi mai (in quel thread).

Cosa succede se, invece di controllare lo stato di una variabile, chiamo un metodo che restituisce il valore della variabile? Per esempio:

static int num = 0; 

... 

void foo() 
{ 
    while(getNum() == 0) 
    { 
     // do something (or nothing) 
    } 
} 

avrei ancora fare num una variabile instabile? o il compilatore riconosce che dal momento che sto usando un metodo per accedere a quella variabile num, non memorizzerà il risultato nella cache?

Qualcuno ha qualche idea?

Grazie in anticipo,

~ Julian

modificare: dentro il mio ciclo, mentre ho rimosso la chiamata sonno e lo ha sostituito con qualcosa di generico come un commento a fare qualcosa (o nulla)

+0

Controllare questa domanda (http://stackoverflow.com/questions/3148319/is-volatile-richiesta-per-combinato-memoria-accesso-via-accesso-funzione). – gablin

risposta

4

No, volatile non è mai necessario finché si esegue la sincronizzazione necessaria.

Chiamare le funzioni di sincronizzazione della libreria di thread, qualunque sia la loro piattaforma, dovrebbe fare in modo di invalidare localmente i valori "cache" e fare in modo che il compilatore ricarichi globalmente.

In questo caso particolare, è probabile che sleep abbia un tale effetto, ma non è comunque una buona implementazione. Ci dovrebbe essere una variabile di condizione su num, proteggerla con una funzione di setter e fare in modo che la funzione di setter invii un segnale a foo.

Per quanto riguarda la domanda specifica, se la funzione nasconde l'accesso dall'ottimizzazione è estremamente dipendente dall'implementazione e dalla situazione. La soluzione migliore è compilare la funzione getter in una chiamata separata del compilatore, ma anche in questo caso, non c'è modo di garantire che l'ottimizzazione interprocedurale non si verifichi. Ad esempio, alcune piattaforme potrebbero inserire codice IR nei file .o ed eseguire la generazione del codice nella fase "linker".

Disclaimer.

Parole chiave di cui sopra: 1. fino a quando si sta facendo la sincronizzazione necessaria e 2. probabilità di avere un tale effetto.

1: sleep o un ciclo di occupato vuoto non sono "sincronizzazione necessaria". Questo non è il modo corretto di scrivere un programma multithread, punto. Quindi, in questi casi, potrebbe essere necessaria una volatilità.

2: Sì, sleep potrebbe non essere conteggiato dall'implementazione di una funzione I/O e potrebbe anche essere etichettato come puro e privo di effetti collaterali. In tal caso, sarebbe necessario il numero volatile globale. Tuttavia, dubito che siano state effettivamente distribuite eventuali implementazioni che possano interrompere i loop sleep in questo modo, poiché sono sfortunatamente comuni.

+5

+1: 'volatile' è * non * un sostituto di opportuni costrutti di sincronizzazione multithreading. –

+0

"La chiamata alle funzioni di sincronizzazione della libreria di thread dovrebbe fare in modo di invalidare i valori" memorizzati nella cache "localmente e rendere il compilatore di ricarica globale". Questo è semplicemente sbagliato o corretto in un modo sottile che ha bisogno di ulteriori spiegazioni ... La mia comprensione è che è sbagliato, chiamare 'sleep' NON costringerà il compilatore a ricaricare il valore del globale. Infatti, essendo parte delle librerie standard, potrebbe sapere che non ha effetti collaterali e in quanto tale il compilatore può dedurre che la variabile non ha bisogno di essere riletta. –

+0

Leggi la domanda/le risposte collegate da gablin in un commento alla domanda. –

-1

tecnicamente deve essere contrassegnato come volatile. I compilatori sono liberi di fare ciò che vogliono per ottimizzare il codice finché continua a conformarsi alle specifiche della macchina astratta C++. Un compilatore conforme, con risorse sufficienti, potrebbe integrare tutte le istanze di getNum, spostare il valore di num in un registro (o semplicemente, notando che non è mai stato effettivamente modificato da alcun codice, trattarlo come una costante) per l'intera durata del programma .

In pratica, nessuna CPU (corrente) ha abbastanza registri liberi che anche il compilatore di ottimizzazione più aggressivo avrebbe scelto di farlo. Ma se si desidera la correttezza, è necessario volatile.

+0

Ok, ma prendiamo una situazione leggermente diversa. Supponiamo che lo stat * num * ora diventi una variabile membro privata accessibile tramite getNum e, di conseguenza, il compilatore non possa accedere inline. Avrei ancora bisogno di contrassegnare * num * come volatile? ... Penso che la risposta corretta in entrambi i casi (quella originale e quella nuova) sia che devo introdurre la sincronizzazione attraverso i blocchi. – jbu

+0

Penso che molti compilatori siano abbastanza intelligenti da comprendere che una variabile globale in un programma multithread può essere cambiata da un'altra discussione! –

+0

Non importa quanto tu sia contorto. La macchina astratta C++ non riconosce i thread. Pertanto una variabile che può essere modificata da un thread diverso viene cambiata al di fuori della macchina astratta corrente. Quindi, per essere corretto, ha bisogno di essere etichettato volatile. –

2

Quello che si propone è fondamentalmente un uso improprio di "volatile", il suo vero scopo è quello di dire al compilatore che la variabile può essere modificata dall'hardware esterno o da un altro processo nel sistema, quindi, deve essere letto veramente dalla memoria ogni volta che è usato.

Non ti proteggerà dalle collisioni di filo, ecc. All'interno del programma, anche se nel tuo caso sembra che tu stia usando il flag per segnalare una richiesta di spegnimento.

In realtà OK per farlo senza sincronizzazione ecc. Purché si sappia che solo un singolo thread di controllo aggiornerà la variabile. Inoltre vorrei usare un po 'di manipolazione per impostare il flag in quanto questo è più probabile che sia "atomico" su più hardware.

num && x'00000001' 
+0

Sì, penso che dovrei usare i meccanismi di sincronizzazione per essere sicuro e corretto. – jbu

0

Purtroppo la semantica volatile è piuttosto sdolcinata. Il concetto di volatile non era realmente pensato per essere utilizzato per il threading.

Potatoswatter è corretto che chiamare le primitive di sincronizzazione del sistema operativo normalmente impedirà al compilatore di ottimizzare di sollevare la lettura di num dal ciclo. Ma funziona per sorta la stessa ragione per cui l'utilizzo di un metodo accessor funziona ... per caso.

Il compilatore vede che si chiama una funzione che non è immediatamente disponibile per l'inlining o l'analisi, quindi deve presumere che qualsiasi variabile che potrebbe essere utilizzata da qualche altra funzione potrebbe essere letta o alterata in quella opaca funzione. Quindi, prima di effettuare la chiamata, il compilatore deve scrivere tutte quelle variabili "globali" in memoria.

In corensic, abbiamo aggiunto una funzione inline a jinx.h che lo fa in modo più diretto. Qualcosa di simile a quanto segue:

inline void memory_barrier() { asm volatile("nop" ::: "memory"); } 

Questo è piuttosto sottile, ma dice effettivamente il compilatore (GCC) che non può sbarazzarsi di questo pezzo di asm opaco e che l'asm opaca in grado di leggere o scrivere qualsiasi livello globale pezzo di memoria visibile. Questo impedisce al compilatore di riordinare carichi/negozi attraverso questo limite.

Per esempio:

memory_barrier(); while (num == 0) { memory_barrier(); ... }

Ora la lettura di num è bloccata. E potenzialmente più importante, è bloccato sul posto rispetto ad altri codici. Così si potrebbe avere:

while (flag == 0) { memory_barrier(); } // spin 
process data[0..N] 

E un altro thread fa:

populate data[0..N] 
memory_barrier(); 
flag = 1; 

PS.Se fai questo tipo di cose (essenzialmente creando le tue primitive di sincronizzazione) le vittorie perf possono essere grandi ma il rischio di qualità è alto. Jinx è particolarmente adatto a trovare bug in queste strutture prive di lock. Quindi potresti voler usarlo o qualche altro strumento per testare questa roba.

PPS. La comunità linux ha un bel post su questo chiamato "volatile considerato dannoso", dai un'occhiata.

+0

"_Ma funziona in qualche modo lo stesso motivo per cui l'utilizzo di un metodo accessor funziona ... per sbaglio." "Per favore, spiega. – curiousguy

Problemi correlati