Condividi tramite


Procedure consigliate per il threading gestito

Il multithreading richiede un'attenta programmazione. Per la maggior parte delle attività, è possibile ridurre la complessità accodando le richieste per l'esecuzione ai thread del pool. Questo argomento illustra situazioni più difficili, ad esempio il coordinamento del lavoro di più thread o la gestione di thread che bloccano.

Annotazioni

A partire da .NET Framework 4, Task Parallel Library e PLINQ forniscono API che riducono la complessità e i rischi della programmazione multithread. Per altre informazioni, vedere Programmazione parallela in .NET.

Stallo e condizione di corsa

Il multithreading risolve i problemi di velocità effettiva e velocità di risposta, ma in questo modo introduce nuovi problemi: deadlock e race condition.

Deadlock

Un deadlock si verifica quando ognuno di due thread tenta di bloccare una risorsa già bloccata. Nessuno dei due thread può compiere ulteriori progressi.

Molti metodi delle classi di threading gestito forniscono timeout che consentono di rilevare i deadlock. Ad esempio, il codice seguente tenta di 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.
}

Condizioni di gara

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 blocco di codice specifico. L'esecuzione del programma molte volte produce risultati diversi e il risultato di una determinata esecuzione non può essere stimato.

Un semplice esempio di condizione di gara è incrementare un campo. Si supponga che una classe disponga di un campo statico privato (Condiviso in Visual Basic) incrementato ogni volta che viene creata un'istanza della classe, usando codice come objCt++; (C#) o objCt += 1 (Visual Basic). Questa operazione richiede il caricamento del valore da objCt in un registro, l'incremento del valore e l'archiviazione in objCt.

In un'applicazione multithreading, un thread che ha caricato e incrementato il valore potrebbe essere preceduto da un altro thread che esegue tutti e tre i passaggi; quando il primo thread riprende l'esecuzione e ne archivia objCt il valore, sovrascrive senza tenere conto del fatto che il valore è cambiato nel frattempo.

Usando i metodi della classe Interlocked, come Interlocked.Increment, questa specifica race condition viene facilmente evitata. Per altre tecniche per la sincronizzazione dei dati tra più thread, vedere Sincronizzazione dei dati per il multithreading.

Le condizioni di competizione si possono verificare anche quando si sincronizzano le attività di più thread. Ogni volta che si scrive una riga di codice, è necessario considerare cosa può accadere se un thread è stato superato prima di eseguire la riga (o prima di una delle singole istruzioni del computer che costituiscono la riga) e un altro thread lo ha superato.

Membri statici e costruttori statici

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

Ad esempio, se un costruttore di classe avvia un nuovo thread e la routine thread chiama un static membro della classe, il nuovo thread si blocca fino al completamento del costruttore della classe.

Questo vale per qualsiasi tipo che può avere un static costruttore.

Numero di processori

Se sono presenti più processori o un solo processore disponibile in un sistema può influenzare l'architettura multithreading. Per altre informazioni, vedere Numero di processori.

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

Raccomandazioni generali

Quando si usano più thread, considerare le linee guida seguenti:

  • Non usare Thread.Abort per terminare altri thread. La chiamata Abort su un altro thread è simile alla generazione di 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. Usa Mutex, ManualResetEvent, AutoResetEvent e Monitor.

  • Non gestire l'esecuzione di thread di lavoro dal programma principale, ad esempio usando eventi. Invece, progetta il programma in modo che i thread di lavoro siano responsabili di attendere fino a quando il lavoro è disponibile, di eseguirlo e di notificare le altre parti del programma quando terminano. Se i thread di lavoro non si bloccano, è consigliabile usare thread del pool. Monitor.PulseAll è utile in situazioni in cui i thread di lavoro si bloccano.

  • Non usare i tipi come oggetti di blocco. Vale a dire, evitare codice come lock(typeof(X)) in C# o SyncLock(GetType(X)) in Visual Basic o l'uso di Monitor.Enter con Type oggetti . Per un determinato tipo, è presente una sola istanza di System.Type per dominio applicativo. Se il tipo su cui si esegue un blocco è pubblico, il codice diverso da quello dell'utente può accettare blocchi su di esso, causando deadlock. Per altri problemi, vedere Procedure consigliate per l'affidabilità.

  • Prestare attenzione quando si bloccano le istanze, ad esempio lock(this) in C# o SyncLock(Me) in Visual Basic. Se un altro codice nell'applicazione, esterno al tipo, accetta un blocco sull'oggetto, potrebbero verificarsi deadlock.

  • Assicurarsi che un thread che sia entrato in un monitor lasci sempre tale monitor, anche se si verifica un'eccezione mentre il thread si trova in esso. L'istruzione lock C# e l'istruzione SyncLock di Visual Basic forniscono questo comportamento automaticamente, utilizzando un blocco finally per assicurarsi che Monitor.Exit venga chiamato. Se non è possibile assicurarsi che venga chiamato Exit , è consigliabile modificare la progettazione in modo da usare Mutex. Un mutex viene rilasciato automaticamente quando il thread che attualmente lo possiede termina.

  • Usare più thread per le attività che richiedono risorse diverse ed evitare di assegnare più thread a una singola risorsa. Ad esempio, qualsiasi attività che implica I/O trae vantaggio dalla presenza di un proprio thread, perché tale thread si blocca durante le operazioni di I/O e quindi consente l'esecuzione di altri thread. L'input dell'utente è un'altra risorsa che trae vantaggio da un thread dedicato. In un computer a processore singolo, un'attività che prevede un calcolo intensivo coesiste con l'input dell'utente e con attività che comportano operazioni di I/O, ma più attività a elevato utilizzo di calcolo si contendono l'una con l'altra.

  • È consigliabile usare i metodi della Interlocked classe per le modifiche di stato semplici, anziché usare l'istruzione lock (SyncLock in Visual Basic). L'istruzione lock è un buon strumento per utilizzo generico, ma la Interlocked classe offre prestazioni migliori per gli aggiornamenti che devono essere atomici. Internamente, esegue un singolo prefisso di blocco se non è presente alcuna contesa. Nelle revisioni del codice controllare il codice simile a quello illustrato negli esempi seguenti. Nel primo esempio viene incrementata una variabile di stato:

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

    È possibile migliorare le prestazioni usando il Increment metodo anziché l'istruzione lock , come indicato di seguito:

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

    Annotazioni

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

    Nel secondo esempio viene aggiornata una variabile di tipo riferimento solo se è 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;
        }
    }
    

    Le prestazioni possono essere migliorate usando invece il CompareExchange metodo , come indicato di seguito:

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

    Annotazioni

    Il sovraccarico del metodo CompareExchange<T>(T, T, T) fornisce un'alternativa sicura per tipo per i tipi di riferimento.

Raccomandazioni per le librerie di classi

Quando si progettano librerie di classi per il multithreading, prendere in considerazione le linee guida seguenti:

  • Evitare la necessità di eseguire la sincronizzazione, se possibile. Questo vale soprattutto per il codice usato di frequente. Ad esempio, un algoritmo potrebbe essere adattato per tollerare una condition di gara anziché eliminarla. La sincronizzazione non necessaria riduce le prestazioni e crea la possibilità del verificarsi di deadlock e race conditions.

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

  • Non rendere il thread di dati dell'istanza sicuro per impostazione predefinita. L'aggiunta di blocchi per creare codice thread-safe riduce le prestazioni, aumenta la contesa di blocco e crea la possibilità che si verifichino deadlock. Nei modelli applicativi comuni, un solo thread alla volta esegue il codice utente, riducendo al minimo la necessità di thread safety. Per questo motivo, le librerie di classi .NET non sono thread-safe per impostazione predefinita.

  • Evitare di fornire metodi statici che modificano lo stato statico. Negli scenari comuni del server, lo stato statico viene condiviso tra le richieste, il che significa che più thread possono eseguire il codice contemporaneamente. Questo apre la possibilità di bug nei thread. Prendere in considerazione l'uso di un modello di progettazione che incapsula i dati in istanze non condivise tra le richieste. Inoltre, se i dati statici vengono sincronizzati, le chiamate tra metodi statici che modificano lo stato possono comportare deadlock o sincronizzazione ridondante, con effetti negativi sulle prestazioni.

Vedere anche