La parola chiave volatile C ++ introduce una barriera di memoria?


86

Capisco che volatileinformi il compilatore che il valore può essere modificato, ma per realizzare questa funzionalità, il compilatore deve introdurre una barriera di memoria per farlo funzionare?

A quanto mi risulta, la sequenza di operazioni sugli oggetti volatili non può essere riordinata e deve essere preservata. Questo sembra implicare che alcuni recinti della memoria sono necessari e che non c'è davvero un modo per aggirare questo. Ho ragione nel dire questo?


C'è una discussione interessante su questa domanda correlata

Jonathan Wakely scrive :

... Gli accessi a variabili volatili distinte non possono essere riordinati dal compilatore fintanto che si verificano in espressioni complete separate ... giusto che volatile è inutile per la sicurezza dei thread, ma non per le ragioni che fornisce. Non è perché il compilatore potrebbe riordinare gli accessi a oggetti volatili, ma perché la CPU potrebbe riordinarli. Le operazioni atomiche e le barriere di memoria impediscono il riordino del compilatore e della CPU

A cui David Schwartz risponde nei commenti :

... Non c'è differenza, dal punto di vista dello standard C ++, tra il compilatore che fa qualcosa e il compilatore che emette istruzioni che inducono l'hardware a fare qualcosa. Se la CPU può riordinare gli accessi ai volatili, lo standard non richiede che il loro ordine venga preservato. ...

... Lo standard C ++ non fa alcuna distinzione su cosa fa il riordino. E non si può sostenere che la CPU possa riordinarli senza alcun effetto osservabile, quindi va bene: lo standard C ++ definisce il loro ordine come osservabile. Un compilatore è conforme allo standard C ++ su una piattaforma se genera codice che fa eseguire alla piattaforma ciò che lo standard richiede. Se lo standard richiede che gli accessi ai volatili non vengano riordinati, una piattaforma che li riordina non è conforme. ...

Il punto è che se lo standard C ++ proibisce al compilatore di riordinare gli accessi a elementi volatili distinti, in base alla teoria che l'ordine di tali accessi fa parte del comportamento osservabile del programma, allora richiede anche al compilatore di emettere codice che proibisce alla CPU di fare così. Lo standard non distingue tra ciò che fa il compilatore e ciò che il codice di generazione del compilatore fa fare alla CPU.

Il che porta a due domande: una delle due è "giusta"? Cosa fanno realmente le implementazioni effettive?


9
Significa principalmente che il compilatore non dovrebbe mantenere quella variabile in un registro. Ogni assegnazione e lettura nel codice sorgente dovrebbe corrispondere agli accessi alla memoria nel codice binario.
Basile Starynkevitch


1
Sospetto che il punto sia che qualsiasi barriera di memoria sarebbe inefficace se il valore venisse memorizzato in un registro interno. Penso che tu debba ancora prendere altre misure di protezione in una situazione simultanea.
Galik

Per quanto ne so, volatile viene utilizzato per variabili che possono essere modificate dall'hardware (spesso utilizzato con microcontrollori). Significa semplicemente che la lettura della variabile non può essere eseguita in un ordine diverso e non può essere ottimizzata. Tuttavia è C, ma dovrebbe essere lo stesso in ++.
Mast

1
@Mast Devo ancora vedere un compilatore che impedisce che le letture delle volatilevariabili vengano ottimizzate dalle cache della CPU. O tutti questi compilatori sono non conformi o lo standard non significa ciò che pensi significhi. (Lo standard non fa distinzione tra ciò che fa il compilatore e ciò che il compilatore fa fare alla CPU. È compito del compilatore emettere codice che, quando eseguito, è conforme allo standard.)
David Schwartz

Risposte:


58

Piuttosto che spiegare cosa volatilefa, permettimi di spiegare quando dovresti usare volatile.

  • Quando all'interno di un gestore di segnali. Perché la scrittura su una volatilevariabile è praticamente l'unica cosa che lo standard ti consente di fare dall'interno di un gestore di segnali. Dal momento che C ++ 11 puoi usare std::atomicper quello scopo, ma solo se l'atomico è privo di lock.
  • Quando si tratta di setjmp secondo Intel .
  • Quando si ha a che fare direttamente con l'hardware e si vuole essere sicuri che il compilatore non ottimizzi le letture o le scritture.

Per esempio:

volatile int *foo = some_memory_mapped_device;
while (*foo)
    ; // wait until *foo turns false

Senza lo volatilespecificatore, il compilatore può ottimizzare completamente il ciclo. Lo volatilespecificatore dice al compilatore che non può presumere che 2 letture successive restituiscano lo stesso valore.

Nota che volatilenon ha nulla a che fare con i thread. L'esempio precedente non funziona se c'era un thread diverso in scrittura *fooperché non è coinvolta alcuna operazione di acquisizione.

In tutti gli altri casi, l'utilizzo di volatiledovrebbe essere considerato non portabile e non consentire più la revisione del codice tranne quando si tratta di compilatori pre-C ++ 11 ed estensioni del compilatore (come lo /volatile:msswitch di msvc , che è abilitato di default in X86 / I64).


5
È più severo di "non si può presumere che 2 letture successive restituiscano lo stesso valore". Anche se leggi una sola volta e / o butti via il valore (i), la lettura deve essere eseguita.
philipxy

1
L'uso nei gestori di segnali e setjmpsono le due garanzie lo standard fa. D'altra parte, l' intento , almeno all'inizio, era supportare l'IO mappato in memoria. Che su alcuni processori può richiedere una recinzione o un membro.
James Kanze

@philipxy Tranne che nessuno sa cosa significa "la lettura". Ad esempio, nessuno crede che una lettura effettiva dalla memoria debba essere eseguita - nessun compilatore che conosco cerca di aggirare le cache della CPU durante gli volatileaccessi.
David Schwartz

@JamesKanze: Non è così. Per quanto riguarda i gestori di segnali, lo standard dice che durante la gestione del segnale solo gli oggetti atomici volatili std :: sig_atomic_t e lock-free hanno valori definiti. Ma dice anche che gli accessi a oggetti volatili sono effetti collaterali osservabili.
philipxy

1
@DavidSchwartz Alcune coppie di architettura del compilatore mappano la sequenza di accessi specificata dallo standard agli effetti effettivi e i programmi funzionanti accedono ai volatili per ottenere quegli effetti. Il fatto che alcune di queste coppie non abbiano una mappatura o una mappatura banale e inutile è rilevante per la qualità delle implementazioni ma non per il punto in questione.
philipxy

25

La parola chiave volatile C ++ introduce una barriera di memoria?

Un compilatore C ++ conforme alla specifica non è necessario per introdurre una barriera di memoria. Il tuo particolare compilatore potrebbe; rivolgi la tua domanda agli autori del tuo compilatore.

La funzione di "volatile" in C ++ non ha nulla a che fare con il threading. Ricorda, lo scopo di "volatile" è disabilitare le ottimizzazioni del compilatore in modo che la lettura da un registro che sta cambiando a causa di condizioni esogene non venga ottimizzata. Un indirizzo di memoria che viene scritto da un thread diverso su una CPU diversa è un registro che cambia a causa di condizioni esogene? No. Ancora una volta, se alcuni autori di compilatori hanno scelto di trattare gli indirizzi di memoria scritti da thread diversi su CPU diverse come se fossero registri che cambiano a causa di condizioni esogene, sono affari loro; non sono tenuti a farlo. Né sono necessari, anche se introduce una barriera di memoria, per garantire, ad esempio, che ogni thread veda un messaggio coerente ordinamento di letture e scritture volatili.

In effetti, volatile è praticamente inutile per il threading in C / C ++. La migliore pratica è evitarlo.

Inoltre: i memory fences sono un dettaglio di implementazione di particolari architetture di processori. In C #, dove volatile è esplicitamente progettato per il multithreading, la specifica non dice che verranno introdotti mezzi recinti, perché il programma potrebbe essere eseguito su un'architettura che non ha recinzioni in primo luogo. Piuttosto, ancora una volta, la specifica rende certe (estremamente deboli) garanzie su quali ottimizzazioni saranno evitate dal compilatore, dal runtime e dalla CPU per porre alcuni vincoli (estremamente deboli) su come verranno ordinati alcuni effetti collaterali. In pratica queste ottimizzazioni vengono eliminate mediante l'uso di mezzi recinti, ma questo è un dettaglio di implementazione soggetto a modifiche in futuro.

Il fatto che ti interessi la semantica di volatile in qualsiasi lingua per quanto riguarda il multithreading indica che stai pensando di condividere la memoria tra i thread. Considera semplicemente di non farlo. Rende il tuo programma molto più difficile da capire e molto più probabile che contenga bug sottili e impossibili da riprodurre.


19
"volatile è praticamente inutile in C / C ++." Affatto! Hai una visione del mondo incentrata sul desktop in modalità utente ... ma la maggior parte del codice C e C ++ viene eseguito su sistemi embedded in cui il volatile è assolutamente necessario per l'I / O mappato in memoria.
Ben Voigt

12
E il motivo per cui l'accesso volatile viene preservato non è semplicemente perché le condizioni esogene possono cambiare le posizioni di memoria. Lo stesso accesso può innescare ulteriori azioni. Ad esempio, è molto comune per una lettura avanzare un FIFO o cancellare un flag di interruzione.
Ben Voigt

3
@ BenVoigt: inutile per affrontare efficacemente i problemi di threading era il mio significato inteso.
Eric Lippert

4
@DavidSchwartz Lo standard ovviamente non può garantire il funzionamento dell'IO mappato in memoria. Ma l'IO mappato in memoria è il motivo per cui è volatilestato introdotto nello standard C. Tuttavia, poiché lo standard non può specificare cose come ciò che accade effettivamente in un "accesso", dice che "ciò che costituisce un accesso a un oggetto che ha un tipo qualificato volatile è definito dall'implementazione". Troppe implementazioni oggi non forniscono una definizione utile di un accesso, che IMHO viola lo spirito dello standard, anche se è conforme alla lettera.
James Kanze

8
Quella modifica è un netto miglioramento, ma la tua spiegazione è ancora troppo focalizzata sulla "memoria potrebbe essere cambiata in modo esogeno". volatilela semantica è più forte di così, il compilatore deve generare ogni accesso richiesto (1.9 / 8, 1.9 / 12), non semplicemente garantire che i cambiamenti esogeni vengano eventualmente rilevati (1.10 / 27). Nel mondo dell'I / O mappato in memoria, una lettura in memoria può avere una logica associata arbitraria, come una proprietà getter. Non ottimizzeresti le chiamate ai getter di proprietà in base alle regole per cui hai dichiarato volatile, né lo Standard lo consente.
Ben Voigt

13

Ciò che David sta trascurando è il fatto che lo standard C ++ specifica il comportamento di diversi thread che interagiscono solo in situazioni specifiche e tutto il resto si traduce in un comportamento indefinito. Una condizione di competizione che coinvolge almeno una scrittura non è definita se non si utilizzano variabili atomiche.

Di conseguenza, il compilatore ha perfettamente il diritto di rinunciare a qualsiasi istruzione di sincronizzazione poiché la tua CPU noterà solo la differenza in un programma che mostra un comportamento indefinito a causa della mancata sincronizzazione.


5
Ben spiegato, grazie. Lo standard definisce solo la sequenza di accessi ai volatili come osservabile fino a quando il programma non ha un comportamento indefinito .
Jonathan Wakely

4
Se il programma ha una corsa di dati, lo standard non prevede requisiti sul comportamento osservabile del programma. Non ci si aspetta che il compilatore aggiunga barriere agli accessi volatili al fine di prevenire gare di dati presenti nel programma, questo è compito del programmatore, sia utilizzando barriere esplicite che operazioni atomiche.
Jonathan Wakely

Perché pensi che lo stia trascurando? Quale parte della mia argomentazione pensi che invalidi? Sono d'accordo al 100% sul fatto che il compilatore ha perfettamente il diritto di rinunciare a qualsiasi sincronizzazione.
David Schwartz

2
Questo è semplicemente sbagliato, o almeno ignora l'essenziale. volatilenon ha nulla a che fare con i thread; il suo scopo originale era quello di supportare IO mappato in memoria. E almeno su alcuni processori, il supporto di IO mappato in memoria richiederebbe recinzioni. (I compilatori non lo fanno, ma questo è un problema diverso.)
James Kanze

@JamesKanze volatileha molto a che fare con i thread: si volatileoccupa della memoria a cui è possibile accedere senza che il compilatore sappia che è possibile accedervi e che copre molti usi del mondo reale dei dati condivisi tra thread su CPU specifiche.
Curiousguy

12

Prima di tutto, gli standard C ++ non garantiscono le barriere di memoria necessarie per ordinare correttamente le operazioni di lettura / scrittura non atomiche. Le variabili volatili sono consigliate per l'utilizzo con MMIO, gestione del segnale, ecc. Nella maggior parte delle implementazioni volatile non è utile per il multi-threading e generalmente non è raccomandato.

Per quanto riguarda l'implementazione degli accessi volatili, questa è la scelta del compilatore.

Questo articolo , che descrive il comportamento di gcc , mostra che non è possibile utilizzare un oggetto volatile come barriera di memoria per ordinare una sequenza di scritture nella memoria volatile.

Per quanto riguarda il comportamento di icc ho scoperto che questa fonte dice anche che volatile non garantisce l'ordinazione degli accessi alla memoria.

Il compilatore Microsoft VS2013 ha un comportamento diverso. Questa documentazione spiega come volatile impone la semantica di rilascio / acquisizione e abilita l'uso di oggetti volatili in blocchi / rilasci su applicazioni multi-thread.

Un altro aspetto che deve essere preso in considerazione è che lo stesso compilatore può avere un comportamento diverso rispetto a. a volatile a seconda dell'architettura hardware di destinazione . Questo post riguardante il compilatore MSVS 2013 afferma chiaramente le specifiche della compilazione con volatile per piattaforme ARM.

Quindi la mia risposta a:

La parola chiave volatile C ++ introduce una barriera di memoria?

sarebbe: non garantito, probabilmente no, ma alcuni compilatori potrebbero farlo. Non dovresti fare affidamento sul fatto che lo faccia.


2
Non impedisce l'ottimizzazione, impedisce solo al compilatore di modificare i carichi e gli archivi oltre determinati vincoli.
Dietrich Epp

Non è chiaro cosa stai dicendo. Stai dicendo che accade su alcuni compilatori non specificati che volatileimpedisce al compilatore di riordinare carichi / archivi? O stai dicendo che lo standard C ++ lo richiede? E in quest'ultimo caso, puoi rispondere al mio argomento contrario citato nella domanda iniziale?
David Schwartz

@DavidSchwartz Lo standard impedisce un riordino (da qualsiasi fonte) degli accessi tramite un volatilelvalue. Dal momento che lascia la definizione di "accesso" all'implementazione, tuttavia, questo non ci guadagna molto se l'implementazione non si cura.
James Kanze

Penso che alcune versioni dei compilatori MSC abbiano implementato la semantica di fence per volatile, ma non c'è fence nel codice generato dal compilatore in Visual Studios 2012.
James Kanze

@JamesKanze Che fondamentalmente significa che l'unico comportamento portatile di volatileè quello specificamente enumerato dallo standard. ( setjmp, segnali e così via.)
David Schwartz

7

Il compilatore inserisce solo un recinto di memoria sull'architettura Itanium, per quanto ne so.

La volatileparola chiave è realmente utilizzata al meglio per modifiche asincrone, ad esempio gestori di segnali e registri mappati in memoria; di solito è lo strumento sbagliato da usare per la programmazione multithread.


1
Una specie di. "il compilatore" (msvc) inserisce una barriera di memoria quando viene scelta come destinazione un'architettura diversa da ARM e viene utilizzata l'opzione / volatile: ms (impostazione predefinita). Vedi msdn.microsoft.com/en-us/library/12a04hfd.aspx . Altri compilatori non inseriscono recinti su variabili volatili per quanto ne so. L'uso di volatile dovrebbe essere evitato a meno che non si tratti direttamente di hardware, gestori di segnali o compilatori non conformi a c ++ 11.
Stefan

@Stefan No. volatileè estremamente utile per molti usi che non hanno mai a che fare con l'hardware. Ogni volta che si desidera che l'implementazione generi codice CPU che segue da vicino il codice C / C ++, utilizzare volatile.
Curiousguy

7

Dipende da quale compilatore è "il compilatore". Visual C ++ lo fa, dal 2005. Ma lo Standard non lo richiede, quindi alcuni altri compilatori no.


VC ++ 2012 non sembra per inserire una recinzione: int volatile i; int main() { return i; }genera un principale con esattamente due istruzioni: mov eax, i; ret 0;.
James Kanze

@JamesKanze: quale versione, esattamente? E stai usando opzioni di compilazione non predefinite? Mi affido alla documentazione (prima versione interessata) e (ultima versione) , che menzionano sicuramente la semantica di acquisizione e rilascio.
Ben Voigt

cl /helpdice la versione 18.00.21005.1. La directory in cui si trova è C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC. L'intestazione nella finestra di comando dice VS 2013. Quindi per quanto riguarda la versione ... Le uniche opzioni che ho usato erano /c /O2 /Fa. (Senza il /O2, imposta anche lo stack frame locale. Ma non ci sono ancora istruzioni per il recinto.)
James Kanze

@JamesKanze: ero più interessato all'architettura, ad esempio "Microsoft (R) C / C ++ Optimizing Compiler versione 18.00.30723 per x64" Forse non c'è nessuna barriera perché x86 e x64 hanno garanzie di coerenza della cache abbastanza forti nel loro modello di memoria per cominciare ?
Ben Voigt

Può essere. Non lo so davvero. Il fatto che l'ho fatto in main, in modo che il compilatore potesse vedere l'intero programma e sapere che non c'erano altri thread, o almeno nessun altro accesso alla variabile prima del mio (quindi non potevano esserci problemi di cache) poteva concepibile influenzare questo anche, ma in qualche modo ne dubito.
James Kanze

5

Questo è in gran parte dalla memoria e si basa su pre-C ++ 11, senza thread. Ma avendo partecipato alle discussioni sul threading nel comitato, posso dire che non c'è mai stato un intento da parte del comitato che volatilepotesse essere utilizzato per la sincronizzazione tra i thread. Microsoft l'ha proposto, ma la proposta non ha avuto successo.

La specifica chiave di volatileè che l'accesso a un volatile rappresenta un "comportamento osservabile", proprio come l'IO. Allo stesso modo il compilatore non può riordinare o rimuovere un IO specifico, non può riordinare o rimuovere gli accessi a un oggetto volatile (o più correttamente, gli accessi tramite un'espressione lvalue con tipo qualificato volatile). L'intento originale di volatile era, infatti, supportare l'IO mappato in memoria. Il "problema" con questo, tuttavia, è che è l'implementazione definita ciò che costituisce un "accesso volatile". E molti compilatori lo implementano come se la definizione fosse "un'istruzione che legge o scrive in memoria è stata eseguita". Che è una definizione legale, anche se inutile, se l'implementazione lo specifica. (Devo ancora trovare le specifiche effettive per qualsiasi compilatore.

Probabilmente (ed è un argomento che accetto), questo viola l'intento dello standard, poiché a meno che l'hardware non riconosca gli indirizzi come IO mappato in memoria e inibisca qualsiasi riordino, ecc., Non puoi nemmeno usare volatile per IO mappato in memoria, almeno su architetture Sparc o Intel. Nondimeno, nessuno dei comiler che ho visto (Sun CC, g ++ e MSC) emette istruzioni fence o membar. (All'incirca nel periodo in cui Microsoft ha proposto di estendere le regole volatile, penso che alcuni dei loro compilatori abbiano implementato la loro proposta e abbiano emesso istruzioni di recinzione per accessi volatili. Non ho verificato cosa fanno i compilatori recenti, ma non mi sorprenderebbe se dipendesse su qualche opzione del compilatore. La versione che ho controllato, penso fosse VS6.0, non emetteva recinzioni, tuttavia.)


Perché dici semplicemente che il compilatore non può riordinare o rimuovere gli accessi agli oggetti volatili? Sicuramente se gli accessi sono un comportamento osservabile, allora sicuramente è altrettanto importante impedire che la CPU, i buffer di scrittura, il controller di memoria e tutto il resto li riordinino.
David Schwartz

@DavidSchwartz Perché è quello che dice lo standard. Certamente, da un punto di vista pratico, ciò che fanno i compilatori che ho verificato è totalmente inutile, ma le parole standard di donnola sono sufficienti in modo che possano ancora rivendicare la conformità (o potrebbero, se lo documentassero effettivamente).
James Kanze

1
@DavidSchwartz: per I / O esclusivo (o mutex) mappato in memoria alle periferiche, la volatilesemantica è perfettamente adeguata. Generalmente tali periferiche segnalano le loro aree di memoria come non memorizzabili nella cache, il che aiuta con il riordino a livello di hardware.
Ben Voigt

@ BenVoigt Mi sono chiesto in qualche modo di questo: l'idea che il processore in qualche modo "sappia" che l'indirizzo con cui ha a che fare è IO mappato in memoria. Per quanto ne so, Sparcs non ha alcun supporto per questo, quindi renderebbe comunque Sun CC e g ++ su uno Sparc inutilizzabile per IO mappato in memoria. (Quando ho esaminato questo aspetto, ero principalmente interessato a uno Sparc.)
James Kanze

@JamesKanze: Da quel po 'di ricerca che ho fatto, sembra che Sparc abbia intervalli di indirizzi dedicati per "viste alternative" di memoria non memorizzabili nella cache. Finché i tuoi punti di accesso volatili nella ASI_REAL_IOporzione dello spazio degli indirizzi, penso che dovresti essere a posto. (Altera NIOS utilizza una tecnica simile, con bit alti dell'indirizzo che controlla il bypass MMU; sono sicuro che ce ne sono anche altri)
Ben Voigt

5

Non è necessario. Volatile non è una primitiva di sincronizzazione. Disabilita solo le ottimizzazioni, ovvero si ottiene una sequenza prevedibile di letture e scritture all'interno di un thread nello stesso ordine prescritto dalla macchina astratta. Ma le letture e le scritture in thread diversi non hanno ordine in primo luogo, non ha senso parlare di preservare o non preservare il loro ordine. L'ordine tra le testine può essere stabilito dalle primitive di sincronizzazione, si ottiene UB senza di esse.

Un po 'di spiegazione riguardo alle barriere della memoria. Una tipica CPU ha diversi livelli di accesso alla memoria. C'è una pipeline di memoria, diversi livelli di cache, quindi RAM ecc.

Le istruzioni di Membar scaricano la conduttura. Non cambiano l'ordine in cui vengono eseguite le letture e le scritture, ma impone solo l'esecuzione di quelle in sospeso in un dato momento. È utile per i programmi multithread, ma non molto altrimenti.

Le cache sono normalmente automaticamente coerenti tra le CPU. Se si desidera assicurarsi che la cache sia sincronizzata con la RAM, è necessario lo svuotamento della cache. È molto diverso da un membroar.


1
Quindi stai dicendo che lo standard C ++ dice che volatiledisabilita solo le ottimizzazioni del compilatore? Non ha alcun senso. Qualsiasi ottimizzazione che il compilatore può fare può, almeno in linea di principio, essere eseguita altrettanto bene dalla CPU. Quindi, se lo standard dicesse che disabilitava solo le ottimizzazioni del compilatore, ciò significherebbe che non fornirebbe alcun comportamento su cui fare affidamento nel codice portatile. Ma questo ovviamente non è vero perché il codice portatile può fare affidamento sul suo comportamento rispetto a setjmpe segnali.
David Schwartz

1
@DavidSchwartz No, lo standard non dice niente del genere. La disabilitazione delle ottimizzazioni è esattamente ciò che viene comunemente fatto per implementare lo standard. Lo standard richiede che il comportamento osservabile avvenga nello stesso ordine richiesto dalla macchina astratta. Quando la macchina astratta non richiede alcun ordine, l'implementazione è libera di utilizzare qualsiasi ordine o nessun ordine. L'accesso a variabili volatili in thread diversi non viene ordinato a meno che non venga applicata una sincronizzazione aggiuntiva.
n. 'pronomi' m.

1
@DavidSchwartz mi scuso per la formulazione imprecisa. Lo standard non richiede che le ottimizzazioni siano disabilitate. Non ha alcuna nozione di ottimizzazione. Piuttosto, specifica il comportamento che in pratica richiede ai compilatori di disabilitare alcune ottimizzazioni in modo tale che la sequenza osservabile di letture e scritture sia conforme allo standard.
n. 'pronomi' m.

1
Tranne che non lo richiede, perché lo standard consente alle implementazioni di definire "sequenze osservabili di letture e scritture" come vogliono. Se le implementazioni scelgono di definire sequenze osservabili in modo tale che le ottimizzazioni debbano essere disabilitate, allora lo fanno. Se no, allora no. Ottieni una sequenza prevedibile di letture e scritture se, e solo se, l'implementazione ha scelto di dartela.
David Schwartz

1
No, l'implementazione deve definire cosa costituisce un unico accesso. La sequenza di tali accessi è prescritta dalla macchina astratta. Un'implementazione deve preservare l'ordine. Lo standard dice esplicitamente che "volatile è un suggerimento all'implementazione per evitare un'ottimizzazione aggressiva che coinvolga l'oggetto", anche se in una parte non normativa, ma l'intento è chiaro.
n. 'pronomi' m.

4

Il compilatore deve introdurre una barriera di memoria attorno agli volatileaccessi se, e solo se, ciò è necessario per fare gli usi volatilespecificati nel lavoro standard ( setjmp, gestori di segnali e così via) su quella particolare piattaforma.

Si noti che alcuni compilatori vanno ben oltre quanto richiesto dallo standard C ++ per renderli volatilepiù potenti o utili su quelle piattaforme. Il codice portatile non dovrebbe fare affidamento volatileper fare nulla oltre a quanto specificato nello standard C ++.


2

Uso sempre volatile nelle routine di servizio di interrupt, ad esempio l'ISR (spesso codice assembly) modifica alcune posizioni di memoria e il codice di livello superiore che viene eseguito al di fuori del contesto di interrupt accede alla posizione di memoria tramite un puntatore a volatile.

Lo faccio per RAM e IO mappato in memoria.

Sulla base della discussione qui sembra che questo sia ancora un uso valido di volatile ma non ha nulla a che fare con più thread o CPU. Se il compilatore per un microcontrollore "sa" che non ci possono essere altri accessi (es. Tutto è su chip, nessuna cache e c'è solo un core) penserei che una barriera di memoria non è affatto implicita, il compilatore ha solo bisogno di prevenire alcune ottimizzazioni.

Man mano che accumuliamo più cose nel "sistema" che esegue il codice oggetto, quasi tutte le scommesse sono disattivate, almeno è così che ho letto questa discussione. Come potrebbe mai un compilatore coprire tutte le basi?


0

Penso che la confusione intorno al riordino volatile e delle istruzioni derivi dalle 2 nozioni di riordino delle CPU:

  1. Esecuzione fuori ordine.
  2. Sequenza di lettura / scrittura della memoria come vista da altre CPU (riordino nel senso che ogni CPU potrebbe vedere una sequenza diversa).

Volatile influisce sul modo in cui un compilatore genera il codice presupponendo l'esecuzione a thread singolo (questo include gli interrupt). Non implica nulla sulle istruzioni relative alla barriera di memoria, ma piuttosto preclude a un compilatore di eseguire determinati tipi di ottimizzazioni relative agli accessi alla memoria.
Un tipico esempio è il recupero di un valore dalla memoria, invece di usarne uno memorizzato nella cache in un registro.

Esecuzione fuori ordine

Le CPU possono eseguire istruzioni fuori ordine / in modo speculativo a condizione che il risultato finale possa essersi verificato nel codice originale. Le CPU possono eseguire trasformazioni non consentite nei compilatori perché i compilatori possono eseguire solo trasformazioni corrette in tutte le circostanze. Al contrario, le CPU possono verificare la validità di queste ottimizzazioni e ritirarle se si rivelano errate.

Sequenza di lettura / scrittura della memoria vista da altre CPU

Il risultato finale di una sequenza di istruzioni, l'ordine effettivo, deve concordare con la semantica del codice generato da un compilatore. Tuttavia, l'effettivo ordine di esecuzione scelto dalla CPU può essere diverso. L'ordine effettivo visto in altre CPU (ogni CPU può avere una vista diversa) può essere vincolato da barriere di memoria.
Non sono sicuro di quanto l'ordine effettivo e quello effettivo possano differire perché non so fino a che punto le barriere di memoria possano impedire alle CPU di eseguire l'esecuzione fuori ordine.

Fonti:


0

Mentre stavo lavorando a un tutorial video scaricabile online per lo sviluppo di grafica 3D e motore di gioco, lavorando con OpenGL moderno. Abbiamo usato volatileall'interno di una delle nostre classi. Il sito web del tutorial può essere trovato qui e il video che lavora con la volatileparola chiave si trova nella Shader Engineserie video 98. Questi lavori non sono miei ma sono accreditati Marek A. Krzeminski, MASce questo è un estratto dalla pagina di download del video.

"Dato che ora possiamo far girare i nostri giochi in più thread, è importante sincronizzare correttamente i dati tra i thread. In questo video mostro come creare una classe di blocco volatile per garantire che le variabili volatili siano sincronizzate correttamente ..."

E se sei iscritto al suo sito web e hai accesso ai suoi video all'interno di questo video, fa riferimento a questo articolo riguardante l'uso di Volatilecon la multithreadingprogrammazione.

Ecco l'articolo dal collegamento sopra: http://www.drdobbs.com/cpp/volatile-the-multithreaded-programmers-b/184403766

volatile: il migliore amico del programmatore multithread

Di Andrei Alexandrescu, 1 febbraio 2001

La parola chiave volatile è stata ideata per impedire le ottimizzazioni del compilatore che potrebbero rendere il codice non corretto in presenza di determinati eventi asincroni.

Non voglio rovinare il tuo umore, ma questa colonna affronta il temuto argomento della programmazione multithread. Se - come dice la precedente puntata di Generic - la programmazione sicura rispetto alle eccezioni è difficile, è un gioco da ragazzi rispetto alla programmazione multithread.

I programmi che utilizzano più thread sono notoriamente difficili da scrivere, dimostrarsi corretti, eseguire il debug, mantenere e domare in generale. Programmi multithread errati potrebbero funzionare per anni senza problemi, solo per funzionare in modo imprevisto perché sono state soddisfatte alcune condizioni di temporizzazione critiche.

Inutile dire che un programmatore che scrive codice multithread ha bisogno di tutto l'aiuto che può ottenere. Questa colonna si concentra sulle condizioni di gara - una fonte comune di problemi nei programmi multithread - e fornisce approfondimenti e strumenti su come evitarli e, sorprendentemente, il compilatore lavora sodo per aiutarti in questo.

Solo una piccola parola chiave

Sebbene sia gli standard C che C ++ siano notevolmente silenziosi quando si tratta di thread, fanno una piccola concessione al multithreading, sotto forma della parola chiave volatile.

Proprio come la sua controparte più nota const, volatile è un modificatore di tipo. È concepito per essere utilizzato insieme a variabili a cui si accede e modificate in thread diversi. Fondamentalmente, senza volatile, la scrittura di programmi multithread diventa impossibile o il compilatore spreca vaste opportunità di ottimizzazione. È necessaria una spiegazione.

Considera il codice seguente:

class Gadget {
public:
    void Wait() {
        while (!flag_) {
            Sleep(1000); // sleeps for 1000 milliseconds
        }
    }
    void Wakeup() {
        flag_ = true;
    }
    ...
private:
    bool flag_;
};

Lo scopo di Gadget :: Wait sopra è controllare la variabile membro flag_ ogni secondo e tornare quando quella variabile è stata impostata su true da un altro thread. Almeno questo è ciò che intendeva il suo programmatore, ma, ahimè, Wait non è corretto.

Supponiamo che il compilatore capisca che Sleep (1000) è una chiamata in una libreria esterna che non può modificare la variabile membro flag_. Quindi il compilatore conclude che può memorizzare nella cache flag_ in un registro e utilizzare quel registro invece di accedere alla memoria su scheda più lenta. Questa è un'ottimizzazione eccellente per il codice a thread singolo, ma in questo caso danneggia la correttezza: dopo aver chiamato Wait for some Gadget object, sebbene un altro thread chiami Wakeup, Wait verrà eseguito in loop per sempre. Questo perché la modifica di flag_ non si rifletterà nel registro che memorizza flag_ nella cache. L'ottimizzazione è troppo ... ottimista.

La memorizzazione nella cache delle variabili nei registri è un'ottimizzazione molto preziosa che si applica la maggior parte delle volte, quindi sarebbe un peccato sprecarla. C e C ++ ti danno la possibilità di disabilitare esplicitamente tale memorizzazione nella cache. Se si utilizza il modificatore volatile su una variabile, il compilatore non memorizzerà nella cache quella variabile nei registri: ogni accesso raggiungerà l'effettiva posizione di memoria di quella variabile. Quindi tutto ciò che devi fare per far funzionare la combo Wait / Wakeup di Gadget è qualificare flag_ in modo appropriato:

class Gadget {
public:
    ... as above ...
private:
    volatile bool flag_;
};

La maggior parte delle spiegazioni della logica e dell'uso di volatile si ferma qui e ti consiglia di qualificare volatile i tipi primitivi che usi in più thread. Tuttavia, c'è molto di più che puoi fare con volatile, perché fa parte del meraviglioso sistema di tipi di C ++.

Utilizzo di volatile con tipi definiti dall'utente

È possibile qualificare volatile non solo i tipi primitivi, ma anche i tipi definiti dall'utente. In tal caso, volatile modifica il tipo in modo simile a const. (Puoi anche applicare const e volatile allo stesso tipo contemporaneamente.)

A differenza di const, volatile discrimina tra tipi primitivi e tipi definiti dall'utente. Vale a dire, a differenza delle classi, i tipi primitivi supportano ancora tutte le loro operazioni (addizione, moltiplicazione, assegnazione, ecc.) Quando qualificati volatile. Ad esempio, è possibile assegnare un int non volatile a un int volatile, ma non è possibile assegnare un oggetto non volatile a un oggetto volatile.

Illustriamo in un esempio come funziona volatile sui tipi definiti dall'utente.

class Gadget {
public:
    void Foo() volatile;
    void Bar();
    ...
private:
    String name_;
    int state_;
};
...
Gadget regularGadget;
volatile Gadget volatileGadget;

Se pensi che volatile non sia così utile con gli oggetti, preparati a qualche sorpresa.

volatileGadget.Foo(); // ok, volatile fun called for
                  // volatile object
regularGadget.Foo();  // ok, volatile fun called for
                  // non-volatile object
volatileGadget.Bar(); // error! Non-volatile function called for
                  // volatile object!

La conversione da un tipo non qualificato alla sua controparte volatile è banale. Tuttavia, proprio come con const, non puoi tornare da volatile a non qualificato. Devi usare un cast:

Gadget& ref = const_cast<Gadget&>(volatileGadget);
ref.Bar(); // ok

Una classe qualificata volatile dà accesso solo a un sottoinsieme della sua interfaccia, un sottoinsieme che è sotto il controllo dell'implementatore della classe. Gli utenti possono ottenere l'accesso completo all'interfaccia di quel tipo solo utilizzando const_cast. Inoltre, proprio come constness, la volatilità si propaga dalla classe ai suoi membri (ad esempio, volatileGadget.name_ e volatileGadget.state_ sono variabili volatili).

volatile, sezioni critiche e condizioni di gara

Il dispositivo di sincronizzazione più semplice e più utilizzato nei programmi multithread è il mutex. Un mutex espone le primitive Acquire e Release. Dopo aver chiamato Acquire in un thread, qualsiasi altro thread che chiama Acquire verrà bloccato. Successivamente, quando quel thread chiama Release, verrà rilasciato esattamente un thread bloccato in una chiamata Acquire. In altre parole, per un dato mutex, solo un thread può ottenere il tempo del processore tra una chiamata ad Acquire e una chiamata a Release. Il codice in esecuzione tra una chiamata ad Acquire e una chiamata a Release è chiamato sezione critica. (La terminologia di Windows è un po 'confusa perché chiama il mutex stesso una sezione critica, mentre "mutex" è in realtà un mutex tra processi. Sarebbe stato carino se fossero chiamati thread mutex e process mutex.)

I mutex vengono utilizzati per proteggere i dati dalle race condition. Per definizione, una condizione di competizione si verifica quando l'effetto di più thread sui dati dipende dalla modalità di pianificazione dei thread. Le condizioni di competizione vengono visualizzate quando due o più thread competono per l'utilizzo degli stessi dati. Poiché i thread possono interrompersi a vicenda in momenti arbitrari, i dati possono essere danneggiati o interpretati male. Di conseguenza, le modifiche e talvolta gli accessi ai dati devono essere accuratamente protetti con sezioni critiche. Nella programmazione orientata agli oggetti, questo di solito significa che si memorizza un mutex in una classe come variabile membro e lo si utilizza ogni volta che si accede allo stato di quella classe.

I programmatori multithread esperti potrebbero aver sbadigliato leggendo i due paragrafi precedenti, ma il loro scopo è fornire un allenamento intellettuale, perché ora ci collegheremo con la connessione volatile. Lo facciamo tracciando un parallelo tra il mondo dei tipi C ++ e il mondo della semantica del threading.

  • Al di fuori di una sezione critica, qualsiasi thread potrebbe interrompere qualsiasi altro in qualsiasi momento; non c'è controllo, quindi di conseguenza le variabili accessibili da più thread sono volatili. Ciò è in linea con l'intento originale di volatile, ovvero quello di impedire al compilatore di memorizzare involontariamente nella cache i valori utilizzati da più thread contemporaneamente.
  • All'interno di una sezione critica definita da un mutex, solo un thread ha accesso. Di conseguenza, all'interno di una sezione critica, il codice in esecuzione ha una semantica a thread singolo. La variabile controllata non è più volatile: è possibile rimuovere il qualificatore volatile.

In breve, i dati condivisi tra i thread sono concettualmente volatili al di fuori di una sezione critica e non volatili all'interno di una sezione critica.

Si entra in una sezione critica bloccando un mutex. Si rimuove il qualificatore volatile da un tipo applicando const_cast. Se riusciamo a mettere insieme queste due operazioni, creiamo una connessione tra il sistema di tipi di C ++ e la semantica di threading dell'applicazione. Possiamo fare in modo che il compilatore controlli le condizioni di gara per noi.

LockingPtr

Abbiamo bisogno di uno strumento che raccolga un'acquisizione mutex e un const_cast. Sviluppiamo un modello di classe LockingPtr inizializzato con un oggetto volatile obj e un mutex mtx. Durante la sua vita, un LockingPtr mantiene mtx acquisito. Inoltre, LockingPtr offre l'accesso all'obj spogliato dei volatili. L'accesso è offerto in modo intelligente dal puntatore, tramite operatore-> e operatore *. Il const_cast viene eseguito all'interno di LockingPtr. Il cast è semanticamente valido perché LockingPtr mantiene il mutex acquisito per tutta la sua durata.

Per prima cosa, definiamo lo scheletro di una classe Mutex con cui LockingPtr funzionerà:

class Mutex {
public:
    void Acquire();
    void Release();
    ...    
};

Per utilizzare LockingPtr, implementa Mutex utilizzando le strutture di dati native e le funzioni primitive del tuo sistema operativo.

LockingPtr è modellato con il tipo della variabile controllata. Ad esempio, se si desidera controllare un widget, utilizzare un LockingPtr che si inizializza con una variabile di tipo Widget volatile.

La definizione di LockingPtr è molto semplice. LockingPtr implementa un puntatore intelligente non sofisticato. Si concentra esclusivamente sulla raccolta di un const_cast e di una sezione critica.

template <typename T>
class LockingPtr {
public:
    // Constructors/destructors
    LockingPtr(volatile T& obj, Mutex& mtx)
      : pObj_(const_cast<T*>(&obj)), pMtx_(&mtx) {    
        mtx.Lock();    
    }
    ~LockingPtr() {    
        pMtx_->Unlock();    
    }
    // Pointer behavior
    T& operator*() {    
        return *pObj_;    
    }
    T* operator->() {   
        return pObj_;   
    }
private:
    T* pObj_;
    Mutex* pMtx_;
    LockingPtr(const LockingPtr&);
    LockingPtr& operator=(const LockingPtr&);
};

Nonostante la sua semplicità, LockingPtr è un aiuto molto utile per scrivere codice multithread corretto. È necessario definire gli oggetti condivisi tra i thread come volatili e non utilizzare mai const_cast con essi: utilizzare sempre gli oggetti automatici LockingPtr. Illustriamolo con un esempio.

Supponi di avere due thread che condividono un oggetto vettoriale:

class SyncBuf {
public:
    void Thread1();
    void Thread2();
private:
    typedef vector<char> BufT;
    volatile BufT buffer_;
    Mutex mtx_; // controls access to buffer_
};

All'interno di una funzione thread, si utilizza semplicemente un LockingPtr per ottenere l'accesso controllato alla variabile membro buffer_:

void SyncBuf::Thread1() {
    LockingPtr<BufT> lpBuf(buffer_, mtx_);
    BufT::iterator i = lpBuf->begin();
    for (; i != lpBuf->end(); ++i) {
        ... use *i ...
    }
}

Il codice è molto facile da scrivere e da comprendere: ogni volta che è necessario utilizzare buffer_, è necessario creare un LockingPtr che punta ad esso. Una volta che lo fai, hai accesso all'intera interfaccia di Vector.

La parte bella è che se commetti un errore, il compilatore lo indicherà:

void SyncBuf::Thread2() {
    // Error! Cannot access 'begin' for a volatile object
    BufT::iterator i = buffer_.begin();
    // Error! Cannot access 'end' for a volatile object
    for ( ; i != lpBuf->end(); ++i ) {
        ... use *i ...
    }
}

Non è possibile accedere a nessuna funzione di buffer_ finché non si applica un const_cast o non si utilizza LockingPtr. La differenza è che LockingPtr offre un modo ordinato di applicare const_cast a variabili volatili.

LockingPtr è straordinariamente espressivo. Se è necessario chiamare solo una funzione, è possibile creare un oggetto LockingPtr temporaneo senza nome e utilizzarlo direttamente:

unsigned int SyncBuf::Size() {
return LockingPtr<BufT>(buffer_, mtx_)->size();
}

Torna ai tipi primitivi

Abbiamo visto come la volatilità protegge gli oggetti da accessi incontrollati e come LockingPtr fornisce un modo semplice ed efficace per scrivere codice thread-safe. Torniamo ora ai tipi primitivi, che vengono trattati in modo diverso da volatile.

Consideriamo un esempio in cui più thread condividono una variabile di tipo int.

class Counter {
public:
    ...
    void Increment() { ++ctr_; }
    void Decrement() { —ctr_; }
private:
    int ctr_;
};

Se Incremento e Decremento devono essere chiamati da thread diversi, il frammento sopra è bacato. Innanzitutto, ctr_ deve essere volatile. In secondo luogo, anche un'operazione apparentemente atomica come ++ ctr_ è in realtà un'operazione in tre fasi. La memoria stessa non ha capacità aritmetiche. Quando si incrementa una variabile, il processore:

  • Legge quella variabile in un registro
  • Incrementa il valore nel registro
  • Scrive il risultato in memoria

Questa operazione in tre fasi è chiamata RMW (Read-Modify-Write). Durante la parte Modifica di un'operazione RMW, la maggior parte dei processori libera il bus di memoria per consentire ad altri processori di accedere alla memoria.

Se in quel momento un altro processore esegue un'operazione RMW sulla stessa variabile, abbiamo una race condition: la seconda scrittura sovrascrive l'effetto della prima.

Per evitare ciò, puoi fare affidamento, ancora una volta, su LockingPtr:

class Counter {
public:
    ...
    void Increment() { ++*LockingPtr<int>(ctr_, mtx_); }
    void Decrement() { —*LockingPtr<int>(ctr_, mtx_); }
private:
    volatile int ctr_;
    Mutex mtx_;
};

Ora il codice è corretto, ma la sua qualità è inferiore rispetto al codice di SyncBuf. Perché? Perché con Counter, il compilatore non ti avviserà se accedi per errore a ctr_ direttamente (senza bloccarlo). Il compilatore compila ++ ctr_ se ctr_ è volatile, sebbene il codice generato sia semplicemente errato. Il compilatore non è più un tuo alleato e solo la tua attenzione può aiutarti a evitare le condizioni di gara.

Cosa dovresti fare allora? Incapsula semplicemente i dati primitivi che usi in strutture di livello superiore e usa volatile con quelle strutture. Paradossalmente, è peggio usare volatile direttamente con i built-in, nonostante inizialmente questo fosse l'intento d'uso di volatile!

Funzioni membro volatili

Finora, abbiamo avuto classi che aggregano membri di dati volatili; ora pensiamo a progettare classi che a loro volta faranno parte di oggetti più grandi e saranno condivise tra thread. Qui è dove le funzioni dei membri volatili possono essere di grande aiuto.

Quando si progetta la classe, si qualificano in modo volatile solo le funzioni membro che sono thread-safe. È necessario presumere che il codice dall'esterno chiamerà le funzioni volatili da qualsiasi codice in qualsiasi momento. Non dimenticare: volatile equivale a codice multithread libero e nessuna sezione critica; non volatile è uguale a scenario a thread singolo o all'interno di una sezione critica.

Ad esempio, si definisce un widget di classe che implementa un'operazione in due varianti: una thread-safe e una veloce, non protetta.

class Widget {
public:
    void Operation() volatile;
    void Operation();
    ...
private:
    Mutex mtx_;
};

Notare l'uso del sovraccarico. Ora l'utente di Widget può invocare Operation utilizzando una sintassi uniforme per oggetti volatili e ottenere thread safety, o per oggetti normali e ottenere velocità. L'utente deve prestare attenzione nel definire gli oggetti Widget condivisi come volatili.

Quando si implementa una funzione membro volatile, la prima operazione è in genere quella di bloccarla con un LockingPtr. Quindi il lavoro viene svolto utilizzando il fratello non volatile:

void Widget::Operation() volatile {
    LockingPtr<Widget> lpThis(*this, mtx_);
    lpThis->Operation(); // invokes the non-volatile function
}

Sommario

Quando si scrivono programmi multithread, è possibile utilizzare volatile a proprio vantaggio. È necessario attenersi alle seguenti regole:

  • Definisci tutti gli oggetti condivisi come volatili.
  • Non utilizzare volatile direttamente con i tipi primitivi.
  • Quando si definiscono classi condivise, utilizzare funzioni membro volatili per esprimere la sicurezza dei thread.

Se lo fai, e se usi il semplice componente generico LockingPtr, puoi scrivere codice thread-safe e preoccuparti molto meno delle condizioni di gara, perché il compilatore si preoccuperà per te e indicherà diligentemente i punti in cui hai sbagliato.

Un paio di progetti a cui sono stato coinvolto hanno utilizzato volatile e LockingPtr con ottimi risultati. Il codice è chiaro e comprensibile. Ricordo un paio di deadlock, ma preferisco i deadlock alle condizioni di gara perché sono molto più facili da eseguire il debug. Non c'erano praticamente problemi legati alle condizioni di gara. Ma poi non lo sai mai.

Ringraziamenti

Molte grazie a James Kanze e Sorin Jianu che hanno contribuito con idee penetranti.


Andrei Alexandrescu è Development Manager presso RealNetworks Inc. (www.realnetworks.com), con sede a Seattle, WA, e autore dell'acclamato libro Modern C ++ Design. Può essere contattato su www.moderncppdesign.com. Andrei è anche uno degli istruttori del The C ++ Seminar (www.gotw.ca/cpp_seminar).

Questo articolo potrebbe essere un po 'datato, ma fornisce una buona visione di un uso eccellente dell'uso del modificatore volatile nell'uso della programmazione multithread per aiutare a mantenere gli eventi asincroni mentre il compilatore controlla le condizioni di gara per noi. Questo potrebbe non rispondere direttamente alla domanda originale dell'OP sulla creazione di una barriera di memoria, ma ho scelto di postarla come risposta per altri come un eccellente riferimento verso un buon uso del volatile quando si lavora con applicazioni multithread.


0

La parola chiave volatilesignifica essenzialmente che la lettura e la scrittura di un oggetto deve essere eseguita esattamente come scritto dal programma e non ottimizzata in alcun modo . Il codice binario dovrebbe seguire il codice C o C ++: un carico dove questo viene letto, un negozio dove c'è una scrittura.

Significa anche che nessuna lettura dovrebbe risultare in un valore prevedibile: il compilatore non dovrebbe assumere nulla su una lettura anche immediatamente dopo una scrittura sullo stesso oggetto volatile:

volatile int i;
i = 1;
int j = i; 
if (j == 1) // not assumed to be true

volatilepuò essere lo strumento più importante nella casella degli strumenti "C è un linguaggio assembly di alto livello" .

Se la dichiarazione di un oggetto volatile è sufficiente per garantire il comportamento del codice che si occupa di modifiche asincrone dipende dalla piattaforma: CPU diverse danno diversi livelli di sincronizzazione garantita per le normali letture e scritture della memoria. Probabilmente non dovresti provare a scrivere codice multithread di livello così basso a meno che tu non sia un esperto nel settore.

Le primitive atomiche forniscono una bella vista di livello superiore degli oggetti per il multithreading che rende facile ragionare sul codice. Quasi tutti i programmatori dovrebbero usare primitive atomiche o primitive che forniscono mutue esclusioni come mutex, read-write-lock, semafori o altre primitive di blocco.

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.