2009-02-27 7 views
17

Ho letto nella documentazione di MS che l'assegnazione di un valore a 64 bit su un computer Intel a 32 bit non è un'operazione atomica; cioè, l'operazione non è thread-safe. Ciò significa che se due persone assegnano contemporaneamente un valore a un campo statico Int64, il valore finale del campo non può essere previsto.In C# è Int64 utilizzato su un processore a 32 bit pericoloso

Tre parte domanda:

  • è proprio vero?
  • È qualcosa di cui mi preoccuperei nel mondo reale?
  • Se la mia applicazione è multi-thread ho davvero bisogno di racchiudere tutti i miei incarichi Int64 con codice di blocco?
+3

Per le operazioni atomiche su Int64, è possibile utilizzare la classe InterLocked (http://msdn.microsoft.com/en-us/library/system.threading.interlocked.add.aspx). –

risposta

18

Non si tratta di tutte le variabili incontrate. Se alcune variabili vengono utilizzate come stato condiviso o qualcosa (inclusi, ma non limitati a alcuni campistatic), è necessario risolvere questo problema. È completamente non problematico per le variabili locali che non vengono issate come conseguenza dell'essere chiusi in una chiusura o in una trasformazione iteratrice e vengono utilizzati da una singola funzione (e quindi, da un singolo thread) alla volta.

+0

Questo è corretto, tuttavia potrebbe non essere chiaro il perché. Un Int64 è ereditato dal sistema.ValueType, che significa che il valore è memorizzato nello stack. Poiché ogni thread riceve il proprio stack di chiamate, ogni thread ha il proprio valore, anche quando chiama la stessa funzione. – codekaizen

+0

imagine class X {int n; }. È un riferimento o un tipo di valore? Sarà archiviato in heap o in pila? –

+0

DK, non penso che questa sia una domanda pertinente, ma le classi sono tipi di riferimento e sono archiviate in un heap. Se si tiene un riferimento a una classe in un solo thread, non si dovrebbe comunque preoccuparsi di problemi di blocco. –

7

MSDN:

Assegnazione di un'istanza di questo tipo è non infilare sicuro su tutto l'hardware piattaforme perché il binario rappresentazione di tale istanza potrebbe essere troppo grande per assegnare in un'unica operazione atomica.

Ma anche:

Come con qualsiasi altro tipo, lettura e scrittura su una variabile condivisa che contiene un'istanza di questo tipo deve essere protetto da una serratura per garantire sicurezza thread.

+2

Vero, la parola chiave è ** variabile condivisa **. –

1

Su una piattaforma x86 a 32 bit, il più grande pezzo di memoria di dimensioni atomiche è di 32 bit.

Ciò significa che se qualcosa scrive o legge da una variabile di 64 bit è possibile che tale lettura/scrittura venga anticipata durante l'esecuzione.

  • Ad esempio, si inizia ad assegnare un valore a una variabile a 64 bit.
  • Dopo aver scritto i primi 32 bit, il sistema operativo decide che un altro processo otterrà il tempo della CPU.
  • Il processo successivo tenta di leggere la variabile a cui si stava assegnando.

Questa è solo una possibile condizione di competizione con assegnazione a 64 bit su una piattaforma a 32 bit.

Tuttavia, anche con variabili a 32 bit ci possono essere condizioni di gara con lettura e scrittura per cui qualsiasi variabile condivisa deve essere sincronizzata in qualche modo per risolvere queste condizioni di gara.

+0

"Su una piattaforma x86 a 32 bit, il più grande pezzo di memoria di dimensioni atomiche è di 32 bit." - È sbagliato. È possibile scrivere atomicamente 8 byte tramite fstp/mmx/sse. –

0

È proprio vero?Sì, a quanto pare. Se i registri contengono solo 32 bit e se è necessario memorizzare un valore a 64 bit in una posizione di memoria, saranno necessarie due operazioni di caricamento e due operazioni di archiviazione. Se il processo viene interrotto da un altro processo tra questi due carichi/negozi, l'altro processo potrebbe corrompere metà dei dati! Strano ma vero. Questo è stato un problema su ogni processore mai creato - se il tuo tipo di dati è più lungo dei tuoi registri, avrai problemi di concorrenza.

È qualcosa di cui mi preoccuperei nel mondo reale? Sì e no. Dato che quasi tutti i programmi moderni hanno il proprio spazio di indirizzamento, è necessario preoccuparsi di questo se si sta eseguendo la programmazione multi-thread.

Se la mia applicazione è multi-thread ho davvero bisogno di racchiudere tutti i miei incarichi Int64 con codice di blocco? Purtroppo sì, se vuoi essere tecnico. Di solito è più facile in pratica utilizzare un Mutex o un semaforo attorno a blocchi di codice più grandi rispetto a bloccare ogni singola istruzione dell'insieme su variabili accessibili a livello globale.

2

Se si dispone di una variabile condivisa (ad esempio, come campo statico di una classe o come campo di un oggetto condiviso), e tale campo o oggetto verrà utilizzato per il cross-thread, quindi, sì, si è necessario assicurarsi che l'accesso a tale variabile sia protetto tramite un'operazione atomica. Il processore x86 ha caratteristiche intrinseche per assicurarsi che ciò accada, e questa funzionalità è esposta attraverso i metodi di classe System.Threading.Interlocked.

Ad esempio:

class Program 
{ 
    public static Int64 UnsafeSharedData; 
    public static Int64 SafeSharedData; 

    static void Main(string[] args) 
    { 
     Action<Int32> unsafeAdd = i => { UnsafeSharedData += i; }; 
     Action<Int32> unsafeSubtract = i => { UnsafeSharedData -= i; }; 
     Action<Int32> safeAdd = i => Interlocked.Add(ref SafeSharedData, i); 
     Action<Int32> safeSubtract = i => Interlocked.Add(ref SafeSharedData, -i); 

     WaitHandle[] waitHandles = new[] { new ManualResetEvent(false), 
              new ManualResetEvent(false), 
              new ManualResetEvent(false), 
              new ManualResetEvent(false)}; 

     Action<Action<Int32>, Object> compute = (a, e) => 
              { 
               for (Int32 i = 1; i <= 1000000; i++) 
               { 
                a(i); 
                Thread.Sleep(0); 
               } 

               ((ManualResetEvent) e).Set(); 
              }; 

     ThreadPool.QueueUserWorkItem(o => compute(unsafeAdd, o), waitHandles[0]); 
     ThreadPool.QueueUserWorkItem(o => compute(unsafeSubtract, o), waitHandles[1]); 
     ThreadPool.QueueUserWorkItem(o => compute(safeAdd, o), waitHandles[2]); 
     ThreadPool.QueueUserWorkItem(o => compute(safeSubtract, o), waitHandles[3]); 

     WaitHandle.WaitAll(waitHandles); 
     Debug.WriteLine("Unsafe: " + UnsafeSharedData); 
     Debug.WriteLine("Safe: " + SafeSharedData); 
    } 
} 

I risultati:

Unsafe: -24.050,275641 millions sicuro: 0

In una nota interessante, mi sono imbattuto questo in modalità x64 su Vista 64. Questo mostra che i campi a 64 bit vengono trattati come campi a 32 bit dal runtime, ovvero le operazioni a 64 bit non sono atomiche. Qualcuno sa se questo è un problema CLR o un problema x64?

+0

Come hanno sottolineato Jon Skeet e Ben S, potrebbe verificarsi la condizione di competizione tra letture e scritture, quindi non è possibile dedurre che le scritture non fossero atomiche. –

+0

Non capisco ... quell'argomento va in ogni caso. Per quanto posso dire, i dati sono ancora sbagliati. Se si esegue l'esempio, è ovvio che i dati finiscono male a causa di operazioni non atomiche. – codekaizen

+0

Il problema non è CLR o x64. È con il tuo codice. Quello che stai cercando di fare è leggere atomicamente + aggiungi/sottrai + scrivi. Mentre in x64 ti è garantita la lettura/scrittura atomica di int64. Di nuovo, questo è diverso dalla lettura atomica + aggiungi + scrivi. –

12

Anche se le scritture erano atomiche, è probabile che sia ancora necessario estrarre un lucchetto ogni volta che si accede alla variabile. Se non lo facessi, dovresti almeno fare la variabile volatile per assicurarti che tutti i thread vedessero il nuovo valore la prossima volta che leggono la variabile (che è quasi sempre quello che vuoi). Ciò ti consente di eseguire set volatili e atomici, ma non appena vuoi fare qualcosa di più interessante, ad esempio aggiungendo 5, tornerai al blocco.

La programmazione di blocco libero è molto, molto difficile da ottenere. È necessario conoscere esattamente quello che stai facendo e mantenere la complessità nel minor numero possibile di codice. Personalmente, raramente provo a provarlo solo per motivi molto noti come l'uso di un inizializzatore statico per inizializzare una raccolta e quindi leggere dalla raccolta senza bloccare.

L'utilizzo della classe Interlocked può essere di aiuto in alcune situazioni, ma è quasi sempre molto più facile estrarre un lucchetto. Le serrature non contestate sono "piuttosto economiche" (ammettiamo che sono costose con più core, ma lo fa anche tutto) - non scherzare con il codice lock-free finché non avrai una buona prova che farà davvero una differenza significativa.