Sincronizzazione di thread (C# e Visual Basic)
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:
Blocco e parole chiave SyncLock
Le istruzioni lock (C#) e SyncLock (Visual Basic) consentono 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.
A un'istruzione lock o SyncLock viene assegnato un oggetto come argomento, seguito da un blocco di codice che dovrà essere eseguito solo da un thread alla volta. Di seguito è riportato un esempio.
Public Class TestThreading
Dim lockThis As New Object
Public Sub Process()
SyncLock lockThis
' Access thread-sensitive resources.
End SyncLock
End Sub
End Class
public class TestThreading
{
private System.Object lockThis = new System.Object();
public void Process()
{
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 senso stretto, l'oggetto fornito 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 sulle istruzioni lock e SyncLock, vedere gli argomenti riportati di seguito:
Monitor
Analogamente alle parole chiave lock e SyncLock, 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. Di seguito è riportato un esempio.
SyncLock x
DoSomething()
End SyncLock
lock (x)
{
DoSomething();
}
Questa dichiarazione equivale a:
Dim obj As Object = CType(x, Object)
System.Threading.Monitor.Enter(obj)
Try
DoSomething()
Finally
System.Threading.Monitor.Exit(obj)
End Try
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 (C#) o SyncLock (Visual Basic) invece di utilizzare direttamente la classe Monitor, sia perché l'uso di lock o SyncLock risulta più conciso, sia perché l'uso di lock o SyncLock garantisce il rilascio del monitor sottostante, anche quando 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.
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.
I thread possono attendere gli eventi mediante la chiamata a uno dei metodi di attesa disponibili, 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.
Imports System.Threading
Module Module1
Dim autoEvent As AutoResetEvent
Sub DoWork()
Console.WriteLine(" worker thread started, now waiting on event...")
autoEvent.WaitOne()
Console.WriteLine(" worker thread reactivated, now exiting...")
End Sub
Sub Main()
autoEvent = New AutoResetEvent(False)
Console.WriteLine("main thread starting worker thread...")
Dim t As New Thread(AddressOf DoWork)
t.Start()
Console.WriteLine("main thread sleeping for 1 second...")
Thread.Sleep(1000)
Console.WriteLine("main thread signaling worker thread...")
autoEvent.Set()
End Sub
End Module
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();
}
}
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.
Classe Interlocked
I metodi della classe Interlocked consentono di evitare i problemi che potrebbero verificarsi quando il tentativo di aggiornare o confrontare lo stesso valore viene effettuato contemporaneamente da più thread. Tramite i metodi di tale classe è possibile incrementare, decrementare, scambiare e confrontare i valori da qualunque thread.
Blocchi ReaderWriter
In alcuni casi è possibile che si desideri bloccare una risorsa solamente durante la scrittura di dati e consentire a più client contemporaneamente di leggere i dati quando non sono in fase di aggiornamento. La classe ReaderWriterLock impone l'accesso esclusivo a una risorsa durante la modifica di tale risorsa da parte di un thread, ma consente accesso non esclusivo per la lettura della risorsa. I blocchi ReaderWriter rappresentano un'alternativa efficace ai blocchi esclusivi, che impongono l'attesa agli altri thread, anche se tali thread non necessitano di aggiornare i dati.
Deadlock
La sincronizzazione dei thread risulta preziosissima nelle applicazioni multithread, ma è possibile che si verifichino deadlock in cui più thread rimangono in reciproca attesa. Un deadlock può essere paragonato a macchine ferme a uno stop di un incrocio in cui ognuno aspetta che l'altro si muova. Evitare i deadlock è fondamentale, la chiave è un'attenta pianificazione. È spesso possibile prevedere situazioni di deadlock creando diagrammi di applicazioni multithread, prima ancora di iniziare la codificazione.
Sezioni correlate
Procedura: utilizzare un pool di thread (C# e Visual Basic)
Come creare un thread utilizzando Visual C# .NET
Come inviare un elemento di lavoro del pool di thread utilizzando Visual C# .NET
Vedere anche
Riferimenti
Istruzione lock (Riferimenti per C#)
Concetti
Applicazioni multithreading (C# e Visual Basic)
Sincronizzazione dei dati per il multithreading