Suggerimenti per l'uso del threading gestito

Il multithreading richiede un'attenta programmazione. È possibile ridurre la complessità della maggior parte delle attività accodando le richieste di esecuzione tramite thread di pool di thread. In questo argomento vengono analizzate situazioni più complesse, come il coordinamento del lavoro di più thread o la gestione di thread che effettuano un blocco.

Nota

A partire da .NET Framework 4, la libreria TPL (Task Parallel Library) e PLINQ specificano API che riducono, in parte, la complessità e i rischi associati alla programmazione multithread. Per altre informazioni, vedere Programmazione parallela in .NET.

Deadlock e race condition

Il multithreading consente di risolvere problemi di trasmissione dei dati e velocità di risposta, ma è anche causa di nuovi problemi: i deadlock e le race condition.

Deadlock

Un deadlock si verifica quando uno di due thread tenta di bloccare una risorsa già bloccata dall'altro. Nessuno dei due è in grado di fare ulteriori progressi.

È possibile rilevare i deadlock tramite i timeout disponibili in molti metodi delle classi di threading gestito. Il codice riportato di seguito, ad esempio, prova ad acquisire un blocco su un oggetto denominato lockObject. Se il blocco non viene ottenuto in 300 millisecondi, Monitor.TryEnter restituisce false.

If Monitor.TryEnter(lockObject, 300) Then  
    Try  
        ' Place code protected by the Monitor here.  
    Finally  
        Monitor.Exit(lockObject)  
    End Try  
Else  
    ' Code to execute if the attempt times out.  
End If  
if (Monitor.TryEnter(lockObject, 300)) {  
    try {  
        // Place code protected by the Monitor here.  
    }  
    finally {  
        Monitor.Exit(lockObject);  
    }  
}  
else {  
    // Code to execute if the attempt times out.  
}  

Race condition

Una race condition è un bug che si verifica quando il risultato di un programma dipende da quale tra due o più thread raggiunge per primo un determinato blocco di codice. L'esecuzione ripetuta del programma produce ogni volta risultati diversi che non è possibile prevedere in anticipo.

Un semplice esempio di race condition è l'incremento di un campo. Si supponga che una classe possieda un campo statico (Shared in Visual Basic) che viene incrementato ogni volta che viene creata un'istanza della classe tramite codice come objCt++; (C#) o objCt += 1 (Visual Basic). Per eseguire questa operazione, è necessario caricare il valore da objCt in un registro, incrementare il valore e archiviarlo in objCt.

In un'applicazione multithreading, un thread che abbia caricato e incrementato il valore potrebbe venire interrotto da un altro thread che esegue tutti e tre i passaggi. Quando il primo thread riprende l'esecuzione e archivia il valore, sovrascrive objCt anche se nel frattempo il valore è stato modificato.

Questa particolare race condition è facilmente evitabile usando i metodi della classe Interlocked, ad esempio Interlocked.Increment. Per informazioni sulle altre tecniche di sincronizzazione dei dati tra più thread, vedere Sincronizzazione dei dati per il multithreading.

Le race condition possono verificarsi anche quando si sincronizzano le attività di più thread. Quando si scrive una riga di codice, è necessario considerare le possibili conseguenze nel caso in cui un thread venisse interrotto prima dell'esecuzione della riga o di una qualsiasi delle singole istruzioni del computer che costituiscono la riga e un altro thread lo raggiungesse.

Membri e costruttori statici

Una classe non viene inizializzata finché non termina l'esecuzione del relativo costruttore (costruttore static in C#, Shared Sub New in Visual Basic). Per impedire l'esecuzione di codice su un tipo non inizializzato, Common Language Runtime blocca tutte le chiamate degli altri thread ai membri static della classe (membri Shared in Visual Basic) fino al termine dell'esecuzione del costruttore.

Se ad esempio il costruttore di una classe avvia un nuovo thread e la routine del thread chiama un membro static della classe, il nuovo thread si bloccherà fino al completamento dell'esecuzione del costruttore.

Ciò è valido per qualsiasi tipo che può avere un costruttore static.

Numero di processori

L'architettura multithreading può essere influenzata dal fatto che nel sistema siano presenti più processori o ve ne sia solo uno. Per altre informazioni, vedere Numero di processori.

Usare la proprietà Environment.ProcessorCount per determinare il numero di processori disponibili in fase di esecuzione.

Raccomandazioni generali

Quando si usano più thread, attenersi alle seguenti linee guida:

  • Non usare Thread.Abort per terminare altri thread. Chiamare Abort su un altro thread equivale a generare un'eccezione su tale thread, senza conoscere il punto raggiunto dal thread nell'elaborazione.

  • Non usare Thread.Suspend e Thread.Resume per sincronizzare le attività di più thread. Usare Mutex, ManualResetEvent, AutoResetEvent e Monitor.

  • Non controllare l'esecuzione dei thread in funzione dal programma principale, ad esempio tramite gli eventi. Progettare invece il programma in modo che i thread in funzione siano responsabili di attendere finché il lavoro non è disponibile, eseguirlo e informare gli altri componenti del programma del termine dell'esecuzione. Se i thread di lavoro non consentono il blocco, prendere in considerazione l'uso di thread del pool di thread. Monitor.PulseAll è utile nelle situazioni in cui i thread di lavoro si bloccano.

  • Non usare tipi come oggetti di blocco. Ciò significa evitare codice come lock(typeof(X)) in C# o SyncLock(GetType(X)) in Visual Basic oppure l'uso di Monitor.Enter con oggetti Type. Per un determinato tipo, è disponibile una sola istanza di System.Type per ogni dominio dell'applicazione. Se il tipo per cui si acquisisce un blocco è pubblico, anche codice diverso dal proprio può acquisire blocchi su di esso, con il conseguente verificarsi di deadlock. Per informazioni su altre problematiche, vedere Reliability Best Practices (Procedure consigliate per l'ottimizzazione dell'affidabilità).

  • Procedere con cautela in caso di blocco sulle istanze, ad esempio lock(this) in C# o SyncLock(Me) in Visual Basic. Se altro codice dell'applicazione, esterno al tipo, acquisisce un blocco sull'oggetto, potrebbero verificarsi situazioni di deadlock.

  • Assicurarsi che un thread entrato in un monitor lasci sempre il monitor, anche se si verifica un'eccezione mentre il thread si trova nel monitor. L'istruzione lock di C# e l'istruzione SyncLock di Visual Basic generano automaticamente questo comportamento, impiegando un blocco finally per garantire che venga chiamato Monitor.Exit. Se non è possibile garantire che Exit verrà chiamato, modificare la progettazione in modo da usare Mutex. Un mutex viene rilasciato automaticamente quando il thread che ne è attualmente proprietario termina.

  • Usare più thread per le attività che richiedono risorse diverse ed evitare di assegnare più thread a una sola risorsa. Alcune attività, ad esempio, che hanno un proprio thread, usufruiscono dei vantaggi di I/O, poiché il thread si bloccherà durante le operazioni di I/O consentendo l'esecuzione di altri thread. Anche l'input utente è una risorsa che trae vantaggio da un thread dedicato. In un computer a processore unico, un'attività che comporta un calcolo a elevato utilizzo di risorse coesiste con l'input utente e con attività che coinvolgono I/O, ma più attività con un elevato utilizzo di risorse competono fra loro.

  • Si prenda in considerazione l'uso dei metodi della classe Interlocked per le modifiche semplici allo stato, invece dell'uso dell'istruzione lock (SyncLock in Visual Basic). L'istruzione lock è un valido strumento generico, ma la classe Interlocked offre prestazioni migliori per gli aggiornamenti che devono essere atomici. Internamente esegue un unico prefisso lock se non esistono conflitti. Nelle analisi di codice controllare il codice simile a quello indicato negli esempi riportati di seguito. Nel primo esempio viene incrementata una variabile di stato:

    SyncLock lockObject  
        myField += 1  
    End SyncLock  
    
    lock(lockObject)
    {  
        myField++;  
    }  
    

    Per migliorare le prestazioni, è possibile usare il metodo Increment invece dell'istruzione lock, come illustrato di seguito:

    System.Threading.Interlocked.Increment(myField)  
    
    System.Threading.Interlocked.Increment(myField);  
    

    Nota

    Usare il metodo Add per gli incrementi atomici maggiori di 1.

    Nel secondo esempio una variabile del tipo di riferimento viene aggiornata solo se corrisponde a un riferimento Null (Nothing in Visual Basic).

    If x Is Nothing Then  
        SyncLock lockObject  
            If x Is Nothing Then  
                x = y  
            End If  
        End SyncLock  
    End If  
    
    if (x == null)  
    {  
        lock (lockObject)  
        {  
            x ??= y;
        }  
    }  
    

    Per migliorare le prestazioni, è invece possibile usare il metodo CompareExchange, come indicato di seguito:

    System.Threading.Interlocked.CompareExchange(x, y, Nothing)  
    
    System.Threading.Interlocked.CompareExchange(ref x, y, null);  
    

    Nota

    L'overload del metodo CompareExchange<T>(T, T, T) offre un'alternativa indipendente dai tipi per i tipo di riferimento.

Suggerimenti per le librerie di classi

Si prendano in considerazione le linee guida riportate di seguito per la progettazione di librerie di classi per il multithreading:

  • Se possibile, evitare che sia necessario eseguire la sincronizzazione. Ciò è valido soprattutto nel caso di codice di uso più frequente. È ad esempio possibile modificare un algoritmo per tollerare una race condition piuttosto che per eliminarla. L'esecuzione di operazioni di sincronizzazione non necessarie riduce le prestazioni e crea la possibilità di deadlock e race condition.

  • Rendere i dati statici (Shared in Visual Basic) thread-safe per impostazione predefinita.

  • Non rendere i dati di istanza thread-safe per impostazione predefinita. Se si aggiungono blocchi per creare codice thread-safe le prestazioni vengono ridotte, aumentano i conflitti di blocco ed è possibile che si verifichino deadlock. Nei modelli applicativi comuni, solo un thread per volta esegue il codice utente riducendo la necessità di thread safety. Per questo motivo le librerie di classi di .NET non sono thread-safe per impostazione predefinita.

  • Evitare di specificare metodi statici che modificano lo stato statico. Negli scenari server comuni, lo stato statico viene condiviso tra le richieste e, pertanto, più thread possono eseguire contemporaneamente tale codice. In questo modo è possibile che si verifichino bug dei thread. È consigliabile provare a usare un modello di progettazione che incapsula i dati nelle istanze che non sono condivise tra le richieste. Se si sincronizzano i dati statici, inoltre, le chiamate tra i metodi statici che modificano lo stato possono determinare deadlock o sincronizzazione ridondante e influire negativamente sulle prestazioni.

Vedi anche