Sincronizzazione di thread (Guida per programmatori C#)
Aggiornamento: novembre 2007
Nelle sezioni seguenti vengono descritte le funzionalità e le classi che è possibile utilizzare per sincronizzare l'accesso alle risorse nelle applicazioni multithreading.
Uno dei vantaggi associati all'utilizzo di più thread in un'applicazione è che ogni thread viene eseguito in modo asincrono. Per le applicazioni Windows, ciò consente di eseguire in background le attività dispendiose in termini di tempo mantenendo su livelli ottimali i tempi di risposta della finestra e dei controlli dell'applicazione. Per le applicazioni server, il multithreading offre la possibilità di gestire ogni richiesta in arrivo con un thread differente. In caso contrario, ogni nuova richiesta verrebbe soddisfatta solo dopo il completamento della richiesta precedente.
La natura asincrona dei thread implica tuttavia che è necessario coordinare l'accesso a risorse quali handle di file, connessioni di rete e memoria. In caso contrario, due o più thread potrebbero accedere contemporaneamente alla stessa risorsa, senza che l'uno rilevi le azioni dell'altro. Il risultato è un danneggiamento imprevedibile dei dati.
Per le semplici operazioni su tipi di dati numerici integrali, la sincronizzazione dei thread può essere effettuata con i membri della classe Interlocked. Per tutti gli altri tipi di dati e per le risorse non thread-safe, il multithreading può essere applicato in modo sicuro solo utilizzando i costrutti descritti nel presente argomento.
Per informazioni generali sulla programmazione di applicazioni multithreading, vedere:
Parola chiave lock
La parola chiave lock consente di assicurarsi che un blocco di codice venga eseguito fino al completamento senza subire interruzioni da altri thread. Questo risultato si ottiene specificando un blocco a esclusione reciproca per un determinato oggetto per la durata del blocco di codice.
Un'istruzione lock inizia con la parola chiave lock, cui viene assegnato un oggetto come argomento, seguita da un blocco di codice che dovrà essere eseguito solo da un thread alla volta. Esempio:
public class TestThreading
{
private System.Object lockThis = new System.Object();
public void Function()
{
lock (lockThis)
{
// Access thread-sensitive resources.
}
}
}
L'argomento fornito alla parola chiave lock deve essere un oggetto basato su un tipo di riferimento e viene utilizzato per definire l'ambito del blocco. Nell'esempio precedente l'ambito del blocco è limitato a questa funzione, perché all'esterno della stessa non sono presenti riferimenti all'oggetto lockThis. Se tale riferimento fosse presente, l'ambito del blocco si estenderebbe fino a tale oggetto. In teoria, l'oggetto fornito a lock viene utilizzato esclusivamente per identificare in modo univoco la risorsa condivisa tra più thread, pertanto può essere un'istanza di classe arbitraria. Nella pratica, tuttavia, l'oggetto rappresenta in genere la risorsa per cui è necessaria la sincronizzazione dei thread. Se ad esempio un oggetto contenitore deve essere utilizzato da più thread, è possibile passare il contenitore a lock per fare in modo che il blocco di codice sincronizzato che segue il blocco acceda al contenitore. Purché altri thread vengano bloccati sullo stesso contenitore prima di accedervi, l'accesso all'oggetto è sincronizzato in modo sicuro.
In generale è preferibile evitare il blocco di un tipo public oppure di istanze di oggetti che esulano dal controllo dell'applicazione. Ad esempio, lock(this) può generare problemi se l'istanza è pubblicamente accessibile, perché il codice fuori controllo potrebbe bloccare anche l'oggetto, creando in questo modo un deadlock nelle situazioni in cui due o più thread attendono il rilascio dello stesso oggetto. Il blocco di un tipo di dati pubblico, anziché di un oggetto, può generare problemi per lo stesso motivo. Il blocco di stringhe letterali è particolarmente rischioso, in quanto queste stringhe sono interne a Common Language Runtime (CLR). Questo significa che esiste un'unica istanza di ogni stringa letterale per l'intero programma, ovvero che lo stesso oggetto rappresenta il valore letterale in tutti i domini applicazione in esecuzione, in tutti i thread. Di conseguenza, un blocco inserito su una stringa il cui contenuto è identico in qualsiasi punto del processo dell'applicazione blocca tutte le istanze di tale stringa nell'applicazione. È pertanto preferibile bloccare un membro privato o protetto che non sia interno. In alcune classi sono disponibili membri specifici per il blocco. Il tipo Array, ad esempio, fornisce la proprietà SyncRoot. Anche diversi tipi di insieme prevedono un membro SyncRoot.
Per ulteriori informazioni sulla parola chiave lock, vedere:
Monitor
Analogamente alla parola chiave lock, i monitor impediscono l'esecuzione simultanea di blocchi di codice da parte di più thread. Il metodo Enter consente a un unico thread di procedere nelle seguenti istruzioni. Tutti gli altri thread risultano bloccati finché il thread in esecuzione non chiama Exit. Questo risultato è analogo a quello che si ottiene con la parola chiave lock. In realtà, la parola chiave lock viene implementata con la classe Monitor. Esempio:
lock (x)
{
DoSomething();
}
Questa dichiarazione equivale a:
System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
DoSomething();
}
finally
{
System.Threading.Monitor.Exit(obj);
}
È in genere preferibile utilizzare la parola chiave lock anziché direttamente la classe Monitor, sia perché lock è più concisa, sia perché lock assicura il rilascio del monitor sottostante, anche se il codice protetto genera un'eccezione. A tal fine viene utilizzata la parola chiave finally, che esegue il blocco di codice associato sia che venga o meno generata un'eccezione.
Per ulteriori informazioni sui monitor, vedere Esempio di tecnologia della sincronizzazione monitor.
Eventi di sincronizzazione e handle di attesa
L'utilizzo di una parola chiave lock o monitor risulta utile per impedire l'esecuzione simultanea di blocchi di codice sensibili ai thread, ma questi costrutti non consentono la comunicazione di un evento tra un thread e l'altro. Per questa funzione sono necessari gli eventi di sincronizzazione, ossia oggetti caratterizzati da uno di due diversi stati, segnalato e non segnalato, che possono essere utilizzati per attivare e sospendere i thread. Per sospendere i thread, è possibile fare in modo che attendano un evento di sincronizzazione con lo stato non segnalato, mentre per attivarli è possibile cambiare lo stato dell'evento in segnalato. L'esecuzione di un thread che tenta di attendere un evento già segnalato continua senza ritardi.
Esistono due tipi di eventi di sincronizzazione: AutoResetEvent e ManualResetEvent. L'unica differenza è che AutoResetEvent passa automaticamente dallo stato segnalato allo stato non segnalato ogni volta che attiva un thread. Viceversa ManualResetEvent consente l'attivazione di un numero qualsiasi di thread con lo stato segnalato e torna allo stato non segnalato solo quando viene chiamato il proprio metodo Reset.
Per fare in modo che i thread attendano gli eventi, è possibile chiamare un metodo di attesa, ad esempio WaitOne, WaitAny o WaitAll. WaitHandle.WaitOne() lascia il thread in attesa finché un singolo evento non diventa segnalato, WaitHandle.WaitAny() blocca un thread finché uno o più degli eventi indicati non diventano segnalati e WaitHandle.WaitAll() blocca il thread finché tutti gli eventi indicati non diventano segnalati. L'evento diventa segnalato quando viene chiamato il relativo metodo Set.
Nell'esempio riportato di seguito un thread viene creato e avviato mediante la funzione Main. Il nuovo thread attende un evento utilizzando il metodo WaitOne. Il thread viene sospeso finché l'evento non viene segnalato dal thread primario che esegue la funzione Main. Quando il thread diventa segnalato, viene restituito il thread ausiliario. In questo caso, poiché l'evento viene utilizzato solo per l'attivazione di un thread, è possibile utilizzare la classe AutoResetEvent o ManualResetEvent.
using System;
using System.Threading;
class ThreadingExample
{
static AutoResetEvent autoEvent;
static void DoWork()
{
Console.WriteLine(" worker thread started, now waiting on event...");
autoEvent.WaitOne();
Console.WriteLine(" worker thread reactivated, now exiting...");
}
static void Main()
{
autoEvent = new AutoResetEvent(false);
Console.WriteLine("main thread starting worker thread...");
Thread t = new Thread(DoWork);
t.Start();
Console.WriteLine("main thread sleeping for 1 second...");
Thread.Sleep(1000);
Console.WriteLine("main thread signaling worker thread...");
autoEvent.Set();
}
}
Per ulteriori esempi di utilizzo degli eventi di sincronizzazione dei thread, vedere:
Oggetti Mutex
Un mutex è simile a un monitor, in quanto impedisce l'esecuzione simultanea di un blocco di codice da parte di più thread alla volta. In realtà il nome "mutex" deriva dall'inglese "mutually exclusive", che indica l'esecuzione reciproca dall'utilizzo di questi oggetti. A differenza dei monitor, tuttavia, un mutex può essere utilizzato per sincronizzare i thread tra i processi. Il mutex è rappresentato dalla classe Mutex.
Quando viene utilizzato per la sincronizzazione tra i processi, il mutex viene definito mutex denominato, perché deve essere utilizzato in un'altra applicazione e pertanto non può essere condiviso mediante una variabile globale o statica. È necessario assegnargli un nome in modo che entrambe le applicazioni possano accedere allo stesso oggetto mutex.
Anche se è possibile utilizzare un mutex per la sincronizzazione dei thread tra processi, è in genere preferibile utilizzare Monitor, perché i monitor sono stati concepiti appositamente per .NET Framework e pertanto utilizzano le risorse in modo più efficace. Viceversa la classe Mutex è un wrapper per un costrutto Win32. Pur essendo più potente di un monitor, un mutex richiede transizioni di interoperabilità che sono più onerose dal punto di vista del calcolo rispetto a quelle richieste dalla classe Monitor. Per un esempio di utilizzo dei mutex, vedere Mutex.
Sezioni correlate
Procedura: creare e terminare thread (Guida per programmatori C#)
Procedura: utilizzare un pool di thread (Guida per programmatori C#)
Come inviare un elemento di lavoro del pool di thread utilizzando Visual C# .NET