2015-01-24 22 views
6

È a mia conoscenza dell'atomicità che viene utilizzato per assicurarsi che un valore sia letto/scritto interamente anziché in parti. Ad esempio, un valore a 64 bit che è in realtà due DWORD a 32 bit (si consideri x86 qui) deve essere atomico se condiviso tra i thread in modo che entrambi i DWORD vengano letti/scritti contemporaneamente. In questo modo un thread non può leggere la mezza variabile che non viene aggiornata. Come garantisci l'atomicità?Atomicità, volatilità e sicurezza del thread in Windows

Inoltre, è a mia conoscenza che la volatilità non garantisce affatto la sicurezza del filo. È vero?

Ho visto implicare molti luoghi che semplicemente essere atomico/volatile è thread-safe. Non vedo come sia. Non avrò bisogno anche di una barriera di memoria per assicurare che qualsiasi valore, atomico o altro, venga letto/scritto prima che possa essere garantito che sia letto/scritto nell'altro thread?

Così, per esempio diciamo che creo un filo sospeso, fare alcuni calcoli per modificare alcuni valori di una struttura a disposizione del filo e poi riprendere, ad esempio:

HANDLE hThread = CreateThread(NULL, 0, thread_entry, (void *)&data, CREATE_SUSPENDED, NULL); 
data->val64 = SomeCalculation(); 
ResumeThread(hThread); 

Suppongo che questo dipenderebbe da qualsiasi barriere di memoria in ResumeThread? Devo fare uno scambio interbloccato per val64? Cosa succede se il thread è in esecuzione, come cambia le cose?

Sono sicuro che sto chiedendo molto qui ma in sostanza quello che sto cercando di capire è quello che ho chiesto nel titolo: una buona spiegazione per atomicità, volatilità e sicurezza dei thread in Windows. Grazie

+0

'volatile' indica solo una cosa: non ottimizzare gli accessi ripetuti a una variabile. Non pone alcun vincolo sull'atomicità o sul riordino delle operazioni o sulla coerenza della cache. –

risposta

6

è usato per fare un valore sarà letto/scritto interamente

Questa è solo una piccola parte dell'atomicità. Al suo centro significa "non interrompibile", un'istruzione su un processore i cui effetti collaterali non possono essere intercalati con un'altra istruzione. In base alla progettazione, un aggiornamento della memoria è atomico quando può essere eseguito con un singolo ciclo del bus di memoria. Il che richiede che l'indirizzo della posizione di memoria sia allineato allo in modo che un singolo ciclo possa aggiornarlo. Un accesso non allineato richiede lavoro extra, parte dei byte scritti da un ciclo e parte da un altro. Ora non è più ininterrotto.

Ottenere gli aggiornamenti allineati è piuttosto semplice, è una garanzia fornita dal compilatore. O, più in generale, dal modello di memoria implementato dal compilatore. Che semplicemente sceglie gli indirizzi di memoria allineati, a volte lasciando intenzionalmente spazi vuoti inutilizzati di pochi byte per allineare la prossima variabile. Un aggiornamento a una variabile più grande della dimensione nativa del processore non può mai essere atomico.

Ma molto più importanti sono il tipo di istruzioni del processore necessarie per eseguire il threading. Ogni processore implementa una variante di CAS instruction, confronta-e-swap. È l'istruzione atomica di base necessaria per implementare la sincronizzazione. Le primitive di sincronizzazione di livello più elevato, come i monitor (cioè le variabili di condizione), i mutex, i segnali, le sezioni critiche ei semafori sono tutti costruiti su quell'istruzione principale.

Questo è il minimo, un processore di solito ne fornisce di più per rendere semplici le operazioni atomiche. Come l'incremento di una variabile, al suo interno un'operazione interrompibile poiché richiede un'operazione di lettura-modifica-scrittura. Avere bisogno di essere atomici è molto comune, la maggior parte dei programmi C++ si affida ad essa per implementare il conteggio dei riferimenti.

volatilità non garantisce la sicurezza del filo affatto

non lo fa. È un attributo che risale a tempi molto più semplici, quando le macchine avevano solo un singolo processore. Riguarda solo la generazione del codice, in particolare il modo in cui un ottimizzatore di codice tenta di eliminare gli accessi alla memoria e usa invece una copia del valore in un registro del processore. Fa una grande, grande differenza per la velocità di esecuzione del codice, la lettura di un valore da un registro è facilmente 3 volte più veloce di doverlo leggere dalla memoria.

L'applicazione volatile garantisce che il programma di ottimizzazione del codice non consideri il valore nel registro accurato e lo costringa a leggere nuovamente la memoria. Importa solo sul tipo di valori di memoria che non sono stabili da soli, dispositivi che espongono i loro registri attraverso I/O mappati in memoria. E 'stato pesantemente abusato da quel significato fondamentale per cercare di mettere la semantica sui processori con un modello di memoria debole, mentre Itanium è l'esempio più eclatante. Cosa si ottiene con volatile oggi dipende fortemente dallo specifico compilatore e runtime che si utilizza. Non utilizzarlo mai per la sicurezza del thread, utilizzare sempre una primitiva di sincronizzazione.

semplicemente essere atomica/volatile è thread-safe

programmazione sarebbe molto più semplice se fosse vero. Le operazioni atomiche coprono solo le operazioni molto semplici, un vero programma spesso ha bisogno di mantenere un intero oggetto thread-safe. Avere tutti i suoi membri aggiornati atomicamente e non esporre mai una vista dell'oggetto che è parzialmente aggiornata. Qualcosa di semplice come l'iterazione di una lista è un esempio fondamentale, non è possibile avere un altro thread che modifichi l'elenco mentre si guardano i suoi elementi. Questo è il momento in cui devi raggiungere le primitive di sincronizzazione di livello superiore, il tipo che può bloccare il codice fino a che non è sicuro procedere.

I programmi reali spesso soffrono di questa necessità di sincronizzazione e presentano il comportamento Amdahls' law. In altre parole, l'aggiunta di un thread aggiuntivo non rende il programma più veloce. A volte in realtà lo rende più lento. Chiunque trovi una trappola per topi migliore per questo è un Nobel garantito, stiamo ancora aspettando.

2

In generale, C e C++ non forniscono alcuna garanzia su come la lettura o la scrittura di un oggetto "volatile" si comporti in programmi con multithreading. (Il "nuovo" C++ 11 probabilmente lo fa poiché ora include i thread come parte dello standard, ma i thread tradizionalmente non sono stati parte dello standard C o C++.) Usando ipotesi volatili e facendo ipotesi sull'atomicità e la coerenza della cache nel codice che è pensato per essere portabile è un problema. È uno schifo se un particolare compilatore e piattaforma tratterà gli accessi agli oggetti "volatili" in modo thread-safe.

La regola generale è: "volatile" non è sufficiente per garantire l'accesso sicuro ai thread. È necessario utilizzare alcuni meccanismi forniti dalla piattaforma (in genere alcune funzioni o oggetti di sincronizzazione) per accedere ai valori condivisi in thread in modo sicuro.

Ora, in particolare su Windows, in particolare con il VC++ 2005 + compilatore, e in particolare sui sistemi x86 e x64, di accesso a un oggetto primitivo (come un int) può essere fatta thread-safe se:

  1. On Windows a 64 e 32 bit, l'oggetto deve essere di tipo a 32 bit e deve essere allineato a 32 bit.
  2. Su Windows a 64 bit, l'oggetto può anche essere un tipo a 64 bit e deve essere allineato a 64 bit.
  3. Deve essere dichiarato volatile.

Se questi sono veri, gli accessi all'oggetto saranno volatili, atomici e circondati da istruzioni che garantiscono la coerenza della cache. Le dimensioni e le condizioni di allineamento devono essere soddisfatte in modo che il compilatore esegua il codice che esegue operazioni atomiche quando accede all'oggetto. Dichiarare l'oggetto volatile assicura che il compilatore non effettui ottimizzazioni del codice relative alla memorizzazione nella cache di valori precedenti che potrebbe aver letto in un registro e garantisce che il codice generato includa istruzioni appropriate per la barriera di memoria al momento dell'accesso.

Anche così, probabilmente stai ancora meglio usando qualcosa come le funzioni Interlocked * per accedere a piccole cose e muovere oggetti di sincronizzazione standard come Mutexes o CriticalSections per oggetti e strutture di dati più grandi. Idealmente, ottenere librerie e utilizzare strutture dati che già includono blocchi appropriati. Consenti alle tue librerie il sistema operativo & di fare il più duro possibile!

Nel tuo esempio, mi aspetto che tu debba utilizzare un accesso sicuro ai thread per aggiornare val64 se il thread è stato avviato o meno.

Se il filo era già in esecuzione, allora si sarebbe sicuramente necessario un qualche tipo di scrittura thread-safe a val64, sia utilizzando InterchangeExchange64 o simile, o acquisendo e rilasciando qualche tipo di oggetto di sincronizzazione che esegue opportune istruzioni barriera memoria. Allo stesso modo, il thread dovrebbe usare un accessor thread-safe per leggerlo.

Nel caso in cui il thread non è stato ripreso, è un po 'meno chiaro. È possibile che ResumeThread utilizzi o funzioni come una funzione di sincronizzazione e esegua le operazioni di barriera della memoria, ma la documentazione non specifica che lo fa, quindi è meglio presumere che non lo faccia.

Riferimenti:

Su atomicità di 32 e 64 tipi bit allineato ... https://msdn.microsoft.com/en-us/library/windows/desktop/ms684122%28v=vs.85%29.aspx

Su tra le recinzioni di memoria 'volatili' ... https://msdn.microsoft.com/en-us/library/windows/desktop/ms686355%28v=vs.85%29.aspx

+0

Questa è una buona risposta, ma ho contrassegnato la risposta di Hans come corretta poiché è essenzialmente una risposta canonica per l'atomicità, la volatilità e la sicurezza del filo. Apprezzo molto le specifiche di Windows. Se ci fosse un modo per dividere la taglia tra voi due, l'avrei fatto. – loop