Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Aggiornamento: novembre 2007
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 distinte in modo netto e preciso per i seguenti motivi: 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 e 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.
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.
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 Semafori.
Segnalazione
Il modo più semplice per attendere un segnale proveniente da un altro thread consiste nell'eseguire una chiamata al metodo Join, che determina un'interruzione del thread corrente finché l'altro non viene completato. Il metodo Join dispone di due overload che consentono al thread interrotto di abbandonare l'attesa una volta trascorso un determinato intervallo.
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.
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 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.
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.
Nota: |
|---|
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 potenti funzionalità di concorrenza non bloccante. Tuttavia, poiché queste operazioni richiedono un'accurata programmazione a basso livello, nella maggior parte dei casi i blocchi semplici rappresentano la soluzione migliore.
Per una panoramica su questi concetti, vedere Operazioni interlocked.
Vedere anche
Concetti
Sincronizzazione dei dati per il multithreading
Nota: