Componenti thread-safe
La condivisione delle risorse tra i thread rappresenta spesso una necessità nella programmazione multithreading. È possibile ad esempio che per più thread sia necessario accedere a un database condiviso o aggiornare un gruppo di variabili di sistema. Se più thread tentano di accedere contemporaneamente a una risorsa condivisa, può verificarsi una race condition. Tale condizione si verifica quando un thread modifica un risorsa in uno stato non valido e un altro thread tenta di accedere a tale risorsa e di utilizzarla nello stato non valido. Si consideri l'esempio seguente:
Public Class WidgetManipulator
Public TotalWidgets as Integer = 0
Public Sub AddWidget()
TotalWidgets += 1
Console.WriteLine("Total widgets = " & TotalWidgets.ToString)
End Sub
Public Sub RemoveWidgets()
TotalWidgets -= 10
End Sub
End Class
public class WidgetManipulator
{
public int TotalWidgets = 0;
public void AddWidget()
{
TotalWidgets++;
Console.WriteLine("Total widgets = " + TotalWidgets.ToString());
}
public void RemoveWidgets()
{
TotalWidgets -= 10;
}
}
Questa classe espone due metodi. Un metodo, AddWidget, aggiunge 1 al campo TotalWidgets e scrive il valore nella console. Il secondo metodo sottrae 10 dal valore di TotalWidgets. Considerare la situazione che si verificherebbe se due thread tentassero di accedere contemporaneamente alla stessa istanza della classe WidgetManipulator. Un thread può ad esempio chiamare AddWidget proprio quando il secondo metodo chiama RemoveWidgets. In tal caso, il valore di TotalWidgets potrebbe essere modificato dal secondo thread prima che un valore corretto possa essere restituito dal primo thread. Questa race condition può comportare la restituzione di risultati non corretti e il danneggiamento dei dati.
Utilizzo dei blocchi per impedire race condition
È possibile proteggere le sezioni critiche del codice da race condition mediante l'utilizzo di blocchi. Un blocco, rappresentato dall'istruzione SyncLock di Visual Basic o dall'istruzione lock di C#, consente a un singolo thread di esecuzione di ottenere i diritti di esecuzione esclusivi su un oggetto. Nell'esempio seguente viene illustrato l'utilizzo dei blocchi:
SyncLock MyObject
' Insert code that affects MyObject.
End SyncLock
lock(MyObject)
{
// Insert code that affects MyObject.
}
Quando si incontra un blocco, l'esecuzione sull'oggetto specificato (MyObject nell'esempio precedente) viene bloccata fino a quando il thread non ottiene l'accesso esclusivo all'oggetto. Quando viene raggiunta la fine del blocco, il blocco viene liberato e l'esecuzione continua normalmente. È possibile ottenere un blocco solo su un oggetto che restituisce un riferimento. Non è possibile bloccare un tipo valore in questo modo.
Svantaggi dei blocchi
Sebbene garantiscano che più thread non accedano contemporaneamente a un oggetto, i blocchi possono causare un significativo rallentamento delle prestazioni. Si consideri un programma in cui siano in esecuzione più thread differenti. Se ciascun thread deve utilizzare uno specifico oggetto e deve attendere per ottenere un blocco esclusivo su tale oggetto prima dell'esecuzione, l'esecuzione di tutti i thread si arresterà e i thread si accumuleranno, causando una riduzione delle prestazioni. Per queste ragioni, si consiglia di utilizzare i blocchi solo quando si utilizza codice che deve essere eseguito come unità. Potrebbe ad esempio essere necessario aggiornare più risorse interdipendenti. Un tale tipo di codice viene definito atomico. La restrizione dei blocchi al solo codice che deve essere eseguito in modalità atomica consentirà di scrivere componenti multithreading che garantiscano la protezione dei dati e il mantenimento di un livello elevato di prestazioni.
È inoltre necessario fare attenzione a evitare situazioni nelle quali possano verificarsi deadlock. È il caso di più thread che aspettano ognuno il rilascio di risorse condivise da parte di un altro thread. Il thread 1 potrebbe, ad esempio, contenere un blocco sulla risorsa A ed essere in attesa della risorsa B. Il thread 2 potrebbe, invece, disporre di un blocco sulla risorsa B e attendere la risorsa A. In tale circostanza nessun thread potrebbe essere autorizzato a procedere. L'unico modo per evitare situazioni di deadlock è un'accurata programmazione.
Vedere anche
Attività
Procedura: coordinare più thread di esecuzione
Procedura: modificare i controlli dai thread
Procedura dettagliata: modifica di componenti multithreading semplici con Visual Basic
Procedura dettagliata: modifica di componenti multithreading semplici con Visual C#
Riferimenti
Concetti
Cenni preliminari sul modello asincrono basato su eventi