La sincronizzazione dei thread è un elemento fondamentale nella programmazione asincrona, ne ho infatti parlato in vari post.
La soluzione più versatile è sicuramente utilizzare il costrutto [[lock]]
ma, in alcuni casi, l’utilizzo dei metodi della classe Interlocked
permette di ottenere performance decisamente migliori.
Questa classe permette di eseguire operazioni atomiche semplici per variabili condivise da più thread: in particolare permette di effettuare somme o sottrazioni o di effettuare degli assegnamenti anche condizionali.
Essendo operazioni atomiche non c’è alcuna possibilità che un thread legga e modifichi una variabile mentre la sto modificando tramite un metodo Interlocked
.
Dato che sto lavorando a passo livello sulle celle di memoria in tutti i metodi di Interlocked
devo passare il sempre riferimento alla variabile (keyword ref
).
Add, Increment, Decrement
La classe Interlocked
possiede i metodi Add
per sommare una quantità ad una variabile in modo atomico, Increment
e Decrement
per aumentare o diminuire di 1 rispettivamente.
Performance
Andiamo ad analizzare le performance del metodo Add confrontandolo con il rispettivo metodo con il lock utilizzando il fido BenchmarkDotNet.
Ecco i risultati:
Method | Mean | Error | StdDev | Allocated |
---|---|---|---|---|
AddWithNoSync | 662.5 us | 337.9 us | 18.52 us | 3 KB |
AddWithInterlocked | 3,231.2 us | 990.1 us | 54.27 us | 3 KB |
AddWithLock | 8,687.8 us | 8,888.4 us | 487.20 us | 3 KB |
Utilizzando il metodo Add ho un boost di prestazioni di 2x rispetto all’utilizzo di un normale lock.
Exchange
Il metodo Exchange
permette di assegnare un valore ad una variabile, è un assegnamento atomico thread safe.
CompareExchange
Questo metodo racchiude una condizione e un assegnamento nella stessa istruzione (compare-and-swap), il tutto in modo ovviamente atomico.
Se il valore della variabile è uguale a quello del terzo argomento, modificalo a quello del secondo argomento e ritorna poi il valore originale.
è equivalente a
Questo metodo utilizza direttamente istruzioni assembly per confrontare e swappare il contenuto di due indirizzi di memoria, il tutto quindi a bassissimo livello.
La funzione di uguaglianza che viene utilizzata è il confronto diretto in memoria, senza alcuna equality function customizzabile dall’utente.
CompareExchange pattern
Un utilizzo classico di CompareExchange
è aggiornare una property di una classe in modo thread-safe senza utilizzare lock.
Assumiamo quindi di avere il field field
che vogliamo modificare con il metodo f(field)
; il problema è che mentre chiamiamo f(field) qualche altro thread potrebbe modificare field, portando quindi a race condition e bug critici gravi.
Perché interlocked è così veloce?
Vi sono tre tipologie di costrutti di sincronizzazione in C#:
- User-mode: utilizzano istruzioni a basso livello della CPU per coordinare i threads. Dato che utilizza l’hardware questi costrutti sono i più veloci. Esempi sono
volatile
eInterlocked
. - Kernel-mode: utilizzano il sistema operativo e posso avere quindi vari context-switch tra codice managed, codice user-mode e codice nativo kernel-mode. Tutti questi context switch possono influire negativamente sulle performance. Esempi sono
Semaphore
eMutex
. - Costrutti ibridi: sono veloci come i costrutti user-mode se non c’è concorrenza ma c’è uno switch in kernel-mode qualora più thread cerchino di accedere alla stessa risorsa lo stesso tempo. Esempi sono
Monitor
,SemaphoreSlim
,ReaderWriterLockSlim
.
Interlocked è un costrutto user-mode, il costrutto lock invece viene modificato direttamente dal compilatore con:
che quindi utilizza il costrutto ibrido Monitor
: in caso di multipli thread passa a kernel-mode influenzando così le sue performance.
Conclusione
Quando si ha la necessità di sincronizzare dovremo sempre prima capire se è possibile utilizzare Interlocked
: in caso affermativo il suo utilizzo porta ad un notevole miglioramento di prestazioni.
E’ comunque importante sottolineare che non tutto quanto può essere fatto con un lock
può essere fatto con Interlocked
; per cui è sempre necessario capire quale è la soluzione migliore per ogni caso.