Area di riferimento
- Implementing service processes, threading, and application domains in a .NET Framework application
- Develop multithreaded .NET Framework applications
- Interlocked class
- Monitor class
Thread Synchronization
Tutti i thread all'interno del sistema possono avere accesso a risorse condivise come le porte seriali, i file, le finestre ecc. Se un thread richiede l'accesso esclusivo a una risorsa tutti gli altri thread che hanno bisogno della stessa risorsa non potranno continuare il proprio lavoro. Per prevenire che una risorsa condivisa venga corrotta da più thread i programmatori devono utilizzare opportuni costrutti per la gestione della sincronizzazione. Microsoft Windows e il Common Language Runtime offrono molti costrutti. La maggior parte dei costrutti di sincronizzazione fra thread nel CLR sono giusto delle classi wrapper su costrutti di sincronizzazione di Win32. Infatti i threads del CLR non sono altro che dei thread di Windows e quindi questo significa che è Windows a schedulare e controllare la sincronizzazione tra i threads.
Il modo più veloce e più semplice per manipolare dati in modo thread-safe è utilizzare la famiglia di metodi interlocked. La classe Interlocked definisce un certo insieme di metodi statici che posso in modo atomico modificare una variabile.
Vediamo la dichiarazione della classe Interlocked:
public static class Interlocked
{
// Esegue in modo atomico location++
public static extern int Increment(ref int location);
public static extern long Increment(ref long location);
// Esegue in modo atomico location--
public static extern int Decrement(ref int location);
public static extern long Decrement(ref long location);
// Esegue in modo atomico location1 += value
public static int Add(ref int location1, int value);
public static long Add(ref long location1, long value);
// Esegue in modo atomico: if ( location1 == comparand ) location1 = value;
public static extern int CompareExchange(ref int location1, int value, int comparand);
public static T CompareExchange<T>(ref T location1, T value, T comparand) where T: class;
public static extern double CompareExchange(ref double location1, double value, double comparand);
public static extern long CompareExchange(ref long location1, long value, long comparand);
public static extern IntPtr CompareExchange(ref IntPtr location1, IntPtr value, IntPtr comparand);
public static extern object CompareExchange(ref object location1, object value, object comparand);
public static extern float CompareExchange(ref float location1, float value, float comparand);
// Esegue in modo atomico: location1 = value
public static extern double Exchange(ref double location1, double value);
public static extern int Exchange(ref int location1, int value);
public static extern long Exchange(ref long location1, long value);
public static extern IntPtr Exchange(ref IntPtr location1, IntPtr value);
public static extern object Exchange(ref object location1, object value);
public static extern float Exchange(ref float location1, float value);
public static T Exchange<T>(ref T location1, T value) where T: class;
}
Il principale problema della classe Interlocked è che permette di lavorare solo su un piccolo insieme di tipi .NET.
La classe Monitor permette di gestire l'accesso in mutua esclusione a una risorsa che rappresenta la forma più comune di sincronizzazione tra thread. L'utilizzo della classe Monitor è molto semplice basta infatti racchiure il codice che rappresenta la sezione critica all'interno delle chiamate ai metodi Enter ed Exit. Il linguaggio C# offre una sintassi speciale equivalente che utilizza la parola chiave lock. I seguenti due metodi sono equivalenti:
private void Metodo()
{
Object temp = this;
Monitor.Enter(temp);
try
{
// Sezione critica
}
finally
{
Monitor.Exit(temp);
}
}
private void Metodo()
{
lock (this)
{
// Sezione critica
}
}
Utilizzando la classe Monitor è estremamente importante gestire le eccezioni per assicurare che il lock venga sempre rilasciato in modo da evitare una probabile situazione di deadlock. Utilizzare lo statment lock semplifica molto le cose in quanto sarà il compilatore a scrivere questo codice difensivo per noi.
L'oggetto passato ai metodi Enter ed Exit rappresenta l'oggetto utilizzato per la gestione della sincronizzazione. Ciascun reference-type ha una struttura associata ad esso chiamata sync block index che contiene un intero che rappresenta un indice all'interno di un array di sync blocks gestito dal CLR. Un sync block è un pezzo di memoria che può essere associato ad un oggetto. Quando un oggetto è costruito, il synch block index è inizializzato con un numero negativo per indicare che non "punta" a nessun sync block. Poi, quando si esegue il metodo Enter il CLR trova un sync block libero nell'array e setta il sync block index in modo che punti al sync block selezionato. Quando tutti i thread hanno rilasciato un sync block il sycn block index dell'oggetto è resettato a un valore negativo e il sycn block è considerato libero per essere associato in futuro ad un altro oggetto.
La migliore soluzione per risolver il problema della sincronizzazione e per evitare spiacevoli situazioni di deadlock è istanziare un oggetto privato all'interno della classe con il solo scopo di essere utilizzato come parametro per lo statment lock. In questo modo nessuno potrà accedere a tale campo e la gestione della sincronizzazione è demandata esclusivamente allo sviluppatore della classe stessa.