Supponiamo che esista una classe che contiene un campo contatore int pubblico accessibile da più thread, e questo numero aumenterà o diminuirà soltanto.
Quando si aggiunge questo campo, quale dei seguenti schemi dovrebbe essere usato e perché?
- lock(this.locker) this.counter++;
- Interlocked.Increment(ref this.counter);
- Cambia il modificatore di accesso di counter a volatile pubblico
Peggio (nessuno di essi funziona davvero)
Cambia il modificatore di accesso di counter a volatile pubblico
Questo approccio in realtà non è affatto sicuro, e il punto del volatile è che più thread che girano su più CPU buffierano i dati e riorganizzano le istruzioni eseguite.
Se è non volatile, quando la CPU A aumenta di un valore, la CPU B deve aspettare un po' per vedere il valore aumentato, il che può portare a problemi.
Se è volatile, garantisce che entrambe le CPU vedano lo stesso valore contemporaneamente. Ma non evita le operazioni di lettura e scrittura a intersecchi.
Aggiungere valore a una variabile richiede in realtà tre passaggi
1. Lettura, 2. Aggiungi 3. Scrivi
Supponiamo che il thread A legga il valore del contatore come 1 e non sia pronto ad aumentare, allora il thread B legga anch'esso il valore del contatore come 1, e entrambi i thread inizino a eseguire operazioni incrementali e di scrittura. Il valore del contatore finale è 2. Questo non è corretto, entrambi i thread hanno effettuato un'operazione di aumento, e il risultato corretto dovrebbe essere 3. Quindi etichettarlo come volatile è semplicemente insicuro.
È meglio così
lock(this.locker) this.counter++;
In questo modo è sicuro (ricordati di chiudere a chiave ovunque tu voglia accedere a questo bancone, ovviamente). Impedisce a qualsiasi altro thread di eseguire il codice bloccato. E previene anche il problema di sequenziamento delle istruzioni multi-CPU menzionato sopra. Il problema è che il lock è lento nelle prestazioni e, se usi il lock in altri punti non correlati, può bloccare gli altri thread.
Miglior
Interlocked.Increment(ref this.counter);
Questo è sicuro ed efficiente. Esegue lettura, aumento, scrittura di tre operazioni in un atomo senza essere interrotto a metà. Poiché non influisce sugli altri codici, non è necessario ricordare le serrature altrove. Ed è anche molto veloce (come dice MSDN, sulle CPU di oggi spesso è solo un'istruzione).
Ma non sono del tutto sicuro che possa anche risolvere il problema dell'ordine delle istruzioni della CPU, o se debba essere usato insieme a volatile e questo incremento.
Supplemento: Quali problemi risolve meglio la volatilità?
Dato che il volatile non può prevenire il multithreading, cosa può fare? Un buon esempio è che hai due thread, uno scrive sempre su una variabile, supponiamo che questa variabile sia queneLength, e l'altro legge sempre i dati di questa variabile. Se la lunghezza della coda non è volatile, il thread A può essere letto 5 volte, ma il thread B può vedere dati ritardati, o addirittura dati nell'ordine sbagliato. Una soluzione è usare il lock, ma in questo caso puoi anche usare volatile. Questo garantisce che il thread B veda sempre i dati più recenti scritti dal thread A, ma questa logica funziona solo se non li leggi quando lo scrivi, e se non lo scrivi quando lo leggi. Quando più thread vogliono eseguire operazioni di lettura-modifica-scrittura, devi usare Interlock o lock.
|