Cenni preliminari sulle primitive di sincronizzazione
In .NET Framework è disponibile una serie di primitive di sincronizzazione per controllare le interazioni dei thread ed evitare situazioni di race condition. Queste primitive possono essere suddivise approssimativamente in tre categorie: blocco, segnalazione e operazioni interlocked.
Queste categorie non sono definite in modo netto e preciso: alcuni meccanismi di sincronizzazione presentano caratteristiche appartenenti a più categorie; gli eventi che rilasciano un singolo thread alla volta sono equivalenti ai blocchi dal punto di vista funzionale; il rilascio di un qualsiasi blocco può essere considerato come un segnale; infine, le operazioni interlocked possono essere utilizzate per la costruzione di blocchi. La suddivisione in categorie risulta comunque utile.
È importante tenere presente che la sincronizzazione dei thread è un processo di cooperazione. Se anche un solo thread accede direttamente alla risorsa protetta ignorando un meccanismo di sincronizzazione, quest'ultimo non potrà produrre alcun effetto.
In questa panoramica sono incluse le sezioni seguenti:
Blocco
Segnalazione
Tipi di sincronizzazione leggeri
SpinWait
Operazioni interlocked
Blocco
I blocchi consentono di concedere il controllo di una risorsa a un thread alla volta o a un numero di thread specificato. Se un thread richiede un blocco esclusivo e quest'ultimo è in uso, il thread si interrompe finché il blocco non diventa disponibile.
Blocchi esclusivi
La forma più semplice di blocco è costituita dall'istruzione lock di C# (SyncLock in Visual Basic), che controlla l'accesso a un blocco di codice. Quest'ultimo è in genere definito sezione critica. L'istruzione lock viene implementata tramite i metodi Enter e Exit della classe Monitor e utilizza try…catch…finally per garantire il rilascio del blocco.
In genere, l'utilizzo dell'istruzione lock per la protezione di piccoli blocchi di codice, che non si estendono mai oltre un singolo metodo, rappresenta la tecnica ottimale per utilizzare la classe Monitor. Anche se potente, la classe Monitor tende a isolare i blocchi e i deadlock.
Classe Monitor
La classe Monitor fornisce funzionalità aggiuntive che possono essere utilizzate insieme all'istruzione lock:
Il metodo TryEnter consente a un thread di cui è interrotta l'esecuzione, in attesa di una risorsa, di abbandonare l'attesa dopo che è trascorso un determinato intervallo. Restituisce un valore booleano indicante l'esito positivo o negativo, che può essere utilizzato per rilevare ed evitare potenziali deadlock.
Il metodo Wait viene chiamato da un thread in una sezione critica. Cede il controllo della risorsa e si interrompe finché quest'ultima non è di nuovo disponibile.
I metodi Pulse e PulseAll consentono a un thread che sta per rilasciare il blocco o per chiamare Wait di inserire uno o più thread nella coda dei thread pronti, affinché possano acquisire il blocco.
L'impostazione di timeout sugli overload del metodo Wait consente il trasferimento dei thread in attesa nella coda dei thread pronti.
La classe Monitor può fornire la funzionalità di blocco in più domini applicazioni se l'oggetto utilizzato per il blocco deriva da MarshalByRefObject.
Monitor presenta affinità di thread. In altri termini, un thread che è passato sotto il controllo del monitor deve uscirne tramite una chiamata a Exit o Wait.
Non è possibile creare istanze della classe Monitor. I metodi di questa classe sono statici (Shared in Visual Basic) e operano su un oggetto blocco di cui è possibile creare istanze.
Per una panoramica su questi concetti, vedere Monitor.
Classe Mutex
I thread richiedono un oggetto Mutex chiamando un overload del relativo metodo WaitOne. Vengono forniti overload con timeout per consentire ai thread di abbandonare l'attesa. Diversamente dalla classe Monitor, un mutex può essere locale o globale. I mutex globali, noti anche come mutex denominati, risultano visibili attraverso il sistema operativo e possono essere utilizzati per sincronizzare i thread in più processi o domini applicazioni. I mutex locali derivano da MarshalByRefObject e possono essere utilizzati oltre i limiti del dominio applicazione.
Inoltre, la classe Mutex deriva da WaitHandle e può quindi essere utilizzata con i meccanismi di segnalazione forniti da WaitHandle, ad esempio i metodi WaitAll, WaitAny e SignalAndWait.
Analogamente a Monitor, Mutex presenta affinità di thread. A differenza di Monitor, Mutex è un oggetto di cui è possibile creare istanze.
Per una panoramica su questi concetti, vedere Mutex.
Classe SpinLock
A partire da .NET Framework versione 4, è possibile utilizzare la classe SpinLock quando il sovraccarico richiesto da Monitor comporta un calo delle prestazioni. Quando SpinLock incontra una sezione critica bloccata, esegue semplicemente uno spin in un ciclo fino a che il blocco non diventa disponibile. Se il blocco viene mantenuto per un tempo molto breve, lo spin può fornire prestazioni migliori rispetto ai blocchi. Tuttavia, se il blocco viene mantenuto oltre alcune decine di cicli, SpinLock fornirà prestazioni analoghe a quelle di Monitor, ma utilizzerà più cicli della CPU con un possibile calo delle prestazioni di altri thread o processi.
Altri blocchi
Non è necessario che i blocchi siano esclusivi. È spesso utile concedere a un numero limitato di thread l'accesso simultaneo a una risorsa. I semafori e i blocchi in lettura/scrittura sono progettati per controllare questo tipo di accesso alle risorse in pool.
Classe ReaderWriterLock
La classe ReaderWriterLockSlim viene utilizzata quando un thread che modifica i dati, il writer, deve disporre dell'accesso esclusivo a una risorsa. Quando il writer non è attivo, un numero qualsiasi di reader può accedere alla risorsa, ad esempio tramite una chiamata al metodo EnterReadLock. Quando un thread richiede l'accesso esclusivo, ad esempio chiamando il metodo EnterWriteLock, le richieste successive di reader si interrompono finché tutti i reader esistenti non hanno rilasciato il blocco e il writer non ha acquisito e rilasciato il blocco.
ReaderWriterLockSlim presenta affinità di thread.
Per una panoramica su questi concetti, vedere Blocchi in lettura/scrittura.
Classe Semaphore
La classe Semaphore consente a un numero specificato di thread di accedere a una risorsa. Gli eventuali altri thread che richiedono la risorsa si interrompono finché un thread non rilascia il semaforo.
Analogamente alla classe Mutex, Semaphore deriva da WaitHandle. Sempre come Mutex, un oggetto Semaphore può essere locale o globale. Può inoltre essere utilizzato oltre i limiti del dominio applicazione.
A differenza di Monitor, Mutex e ReaderWriterLock, Semaphore non presenta affinità di thread. Di conseguenza, questa classe può essere utilizzata negli scenari in cui il semaforo viene acquisito da un thread e rilasciato da un altro.
Per una panoramica su questi concetti, vedere Semaphore e SemaphoreSlim.
System.Threading.SemaphoreSlim è un semaforo leggero per la sincronizzazione nei limiti di un singolo processo.
Torna all'inizio
Segnalazione
Il modo più semplice per attendere un segnale da un altro thread consiste nel chiamare il metodo Join, che impone un blocco fino al completamento dell'altro thread. Join dispone di due overload che consentono al thread bloccato di uscire dallo stato di attesa una volta trascorso un intervallo di tempo specificato.
Gli handle di attesa forniscono un insieme più ampio di funzionalità di segnalazione e attesa.
Handle di attesa
Gli handle di attesa derivano dalla classe WaitHandle, che deriva a sua volta da MarshalByRefObject. Di conseguenza, questi handle possono essere utilizzati per sincronizzare le attività dei thread oltre i limiti del dominio applicazione.
I thread si interrompono sugli handle di attesa chiamando il metodo di istanza WaitOne o uno dei metodi statici WaitAll, WaitAny e SignalAndWait. La modalità di rilascio dei thread varia a seconda del metodo chiamato e del tipo di handle di attesa.
Per una panoramica su questi concetti, vedere Handle di attesa.
Handle di attesa degli eventi
Gli handle di attesa degli eventi includono la classe EventWaitHandle e le relative classi derivate, nonché AutoResetEvent e ManualResetEvent. I thread vengono rilasciati da un handle di attesa degli eventi quando quest'ultimo riceve un segnale mediante una chiamata al relativo metodo Set oppure tramite il metodo SignalAndWait.
Gli handle di attesa degli eventi vengono reimpostati automaticamente, come un tornello che consente il rilascio di un solo thread ogni volta che riceve un segnale, oppure devono essere reimpostati manualmente, come un cancello che rimane chiuso finché non riceve un segnale e aperto finché non viene chiuso. Come indicato dai nomi stessi, AutoResetEvent e ManualResetEvent rappresentano rispettivamente il primo e il secondo caso. System.Threading.ManualResetEventSlim è un evento leggero per la sincronizzazione nei limiti di un singolo processo.
Un oggetto EventWaitHandle può rappresentare uno dei due tipi di evento e può essere locale o globale. Le classi derivate AutoResetEvent e ManualResetEvent sono sempre locali.
Gli handle di attesa degli eventi non presentano affinità di thread. Qualsiasi thread può inviare un segnale a un handle di attesa degli eventi.
Per una panoramica su questi concetti, vedere EventWaitHandle, AutoResetEvent, CountdownEvent e ManualResetEvent.
Classi Mutex e Semaphore
Poiché le classi Mutex e Semaphore derivano da WaitHandle, possono essere utilizzate con i metodi statici di WaitHandle. Un thread può ad esempio utilizzare il metodo WaitAll per rimanere in attesa finché non si verificano tutte e tre le seguenti situazioni: segnalazione a un EventWaitHandle, rilascio di un Mutex e rilascio di un Semaphore. Analogamente, un thread può utilizzare il metodo WaitAny per rimanere in attesa finché non si verifica una qualsiasi di queste condizioni.
L'invio di un segnale a un Mutex o a un Semaphore equivale al rilascio dell'oggetto stesso. Se uno dei due tipi viene utilizzato come primo argomento del metodo SignalAndWait, viene rilasciato. Nel caso di un Mutex, che presenta affinità di thread, viene generata un'eccezione se il thread chiamante non è il proprietario del mutex. Come indicato in precedenza, i semafori non presentano affinità di thread.
Barriera
La classe Barrier consente di sincronizzare ciclicamente più thread in modo che vengano bloccati tutti nello stesso momento e attendano il completamento di tutti gli altri thread. Una barriera è utile nel caso in cui uno o più thread necessitino dei risultati di un altro thread prima di passare alla fase successiva di un algoritmo. Per ulteriori informazioni, vedere Barriera (.NET Framework).
Torna all'inizio
Tipi di sincronizzazione leggeri
A partire da .NET Framework 4, è possibile utilizzare primitive di sincronizzazione che assicurano prestazioni veloci evitando l'impiego dispendioso, ove possibile, di oggetti del kernel Win32 quali ad esempio gli handle di attesa. In generale, è opportuno utilizzare questi tipi quando i tempi di attesa sono brevi e solo dopo aver provato i tipi di sincronizzazione originali e averli trovati insoddisfacenti. I tipi leggeri non possono essere utilizzati in scenari che richiedono una comunicazione tra processi.
System.Threading.SemaphoreSlim è una versione leggera di System.Threading.Semaphore.
System.Threading.ManualResetEventSlim è una versione leggera di System.Threading.ManualResetEvent.
System.Threading.CountdownEvent rappresenta un evento che viene segnalato quando il relativo conteggio è zero.
System.Threading.Barrier consente la sincronizzazione di più thread senza alcun controllo da parte di un thread master. Una barriera impedisce a ogni thread di proseguire fino a che tutti i thread non hanno raggiunto un punto specificato.
Torna all'inizio
SpinWait
A partire da .NET Framework 4, è possibile utilizzare la struttura System.Threading.SpinWait quando un thread deve attendere che un evento venga segnalato o una condizione soddisfatta, ma soltanto nel caso in cui si prevede che il tempo di attesa effettivo sia inferiore al tempo di attesa richiesto dall'utilizzo di un handle di attesa o da un altro blocco del thread corrente. Tramite SpinWait è possibile specificare un breve periodo di tempo per lo spin durante l'attesa, quindi produrre (ad esempio mediante attesa o sospensione) solo se la condizione non è stata soddisfatta entro il tempo specificato.
Torna all'inizio
Operazioni interlocked
Le operazioni interlocked sono semplici operazioni atomiche eseguite su una posizione della memoria dai metodi statici della classe Interlocked. Queste operazioni comprendono addizioni, incrementi, decrementi, scambi, scambi condizionali basati su confronto e operazioni di lettura per valori a 64 bit su piattaforme a 32 bit.
![]() |
---|
L'atomicità è garantita solo per operazioni singole. Per l'esecuzione di più operazioni come un'unità, è necessario utilizzare un meccanismo di sincronizzazione meno accurato. |
Anche se nessuna di queste operazioni consiste in un blocco o in un segnale, è possibile utilizzarle per costruire blocchi e segnali. Essendo native del sistema operativo Windows, le operazioni interlocked risultano estremamente veloci.
Le operazioni interlocked possono essere utilizzate con garanzie di memoria volatile per la scrittura di applicazioni che presentano efficaci funzionalità di concorrenza non bloccante. Richiedono tuttavia una sofisticata programmazione di basso livello, per cui, nella maggior parte dei casi, i blocchi semplici sono la soluzione migliore.
Per una panoramica su questi concetti, vedere Operazioni interlocked.
Torna all'inizio
Vedere anche
Concetti
Sincronizzazione dei dati per il multithreading
Altre risorse
EventWaitHandle, AutoResetEvent, CountdownEvent e ManualResetEvent