Problemi potenziali nel parallelismo di dati e attività
In molti casi Parallel.For e Parallel.ForEach possono offrire miglioramenti significativi delle prestazioni sui normali cicli sequenziali. Le operazioni necessarie per parallelizzare il ciclo comportano tuttavia delle complessità che possono determinare problemi che in un codice sequenziale sono meno frequenti o addirittura assenti. In questo argomento sono elencati alcuni suggerimenti da tenere presenti quando si scrivono cicli paralleli.
Non presupporre che l'approccio in parallelo sia sempre più veloce
In determinati casi l'esecuzione di un ciclo parallelo potrebbe essere più lenta dell'equivalente sequenziale. La regola generale di base è che per i cicli paralleli con poche iterazioni e con delegati dell'utente veloci raramente si verifica un aumento significativo della velocità di esecuzione. Poiché molti fattori influiscono sulle prestazioni, è comunque consigliabile misurare sempre i risultati effettivi.
Evitare di scrivere in percorsi di memoria condivisi
Nel codice sequenziale spesso si eseguono operazioni di lettura e scrittura su variabili o campi di classe statici. Tuttavia, ogni volta che più thread eseguono un accesso simultaneo a queste variabili, è molto probabile che si verifichino race condition. Anche se è possibile sincronizzare l'accesso alla variabile mediante l'utilizzo di blocchi, il costo di questa sincronizzazione può influire negativamente sulle prestazioni. È pertanto consigliabile evitare o almeno limitare il più possibile l'accesso allo stato condiviso in un ciclo parallelo. Il modo migliore consiste nell'utilizzare gli overload di Parallel.For e Parallel.ForEach che utilizzano una variabile System.Threading.ThreadLocal<T> per archiviare lo stato di thread locale durante l'esecuzione del ciclo. Per ulteriori informazioni, vedere Procedura: scrivere un ciclo Parallel.For con variabili locali dei thread e Procedura: scrivere un ciclo Parallel.ForEach con variabili di thread locali.
Evitare parallelizzazioni eccessive
L'utilizzo dei cicli paralleli comporta costi di sovraccarico dovuti al partizionamento dell'insieme di origine e alla sincronizzazione dei thread di lavoro. I vantaggi della parallelizzazione vengono limitati ulteriormente dal numero di processori nel computer. Non si ottiene alcun aumento di velocità eseguendo più thread con vincoli di calcolo in un unico processore. È pertanto fondamentale evitare la parallelizzazione eccessiva di un ciclo.
La situazione più comune in cui si verifica la parallelizzazione eccessiva è quando si utilizzano cicli annidati. Nella maggior parte dei casi è meglio parallelizzare solo il ciclo esterno, a meno che non sussista almeno una delle condizioni seguenti:
È noto che il ciclo interno è molto lungo.
Si eseguono calcoli dispendiosi in ogni ordine. L'operazione mostrata nell'esempio non è dispendiosa.
È noto che il sistema di destinazione presenta un numero di processori sufficiente per gestire il numero di thread che verranno prodotti dalla parallelizzazione della query su cust.Orders.
In ogni caso, il miglior modo per determinare la forma ottimale della query è tramite lo svolgimento di test e misurazioni.
Evitare chiamate a metodi non thread-safe
La scrittura in metodi di istanza non thread-safe da un ciclo parallelo può comportare un danneggiamento dei dati che può passare inosservato nel programma. Può inoltre comportare la generazione di eccezioni. L'esempio seguente mostra uno scenario in cui più thread tentano di chiamare simultaneamente il metodo FileStream.WriteByte. Tuttavia, la classe non supporta le chiamate simultanee.
Dim fs As FileStream = File.OpenWrite(filepath)
Dim bytes() As Byte
ReDim bytes(1000000)
' ...init byte array
Parallel.For(0, bytes.Length, Sub(n) fs.WriteByte(bytes(n)))
FileStream fs = File.OpenWrite(path);
byte[] bytes = new Byte[10000000];
// ...
Parallel.For(0, bytes.Length, (i) => fs.WriteByte(bytes[i]));
Limitare le chiamate ai metodi thread-safe
La maggior parte dei metodi statici in .NET Framework è thread-safe e può essere chiamata simultaneamente da più thread. Tuttavia, anche in questi casi, la sincronizzazione da applicare può comportare un rallentamento significativo della query.
Nota |
---|
Per verificare ciò basta inserire nelle query alcune chiamate a WriteLine.Anche se questo metodo viene utilizzato a scopo dimostrativo negli esempi della documentazione, è consigliabile evitare di utilizzarlo nei cicli paralleli, a meno che non sia necessario. |
Tenere presente i problemi di affinità di thread
Alcune tecnologie, ad esempio l'interoperabilità COM per i componenti apartment a thread singolo (STA, Single-Threaded Apartment), Windows Form e Windows Presentation Foundation (WPF), impongono restrizioni di affinità di thread che richiedono l'esecuzione del codice in un thread specifico. Sia in Windows Form sia in WPF, ad esempio, l'accesso a un controllo può essere eseguito solo nel thread in cui è stato creato. Ciò significa, ad esempio, che non è possibile aggiornare un controllo elenco da un ciclo parallelo, a meno che non si configuri l'utilità di pianificazione del thread in modo che venga pianificato solo il thread UI. Per ulteriori informazioni, vedere Procedura: pianificare il lavoro in un contesto di sincronizzazione specificato.
Prestare attenzione quando si attendono delegati chiamati da Parallel.Invoke
In determinate circostanze Task Parallel Library rende inline un'attività, ovvero viene eseguito sull'attività nel thread attualmente in esecuzione. Per ulteriori informazioni, vedere Utilità di pianificazione delle attività. Questa ottimizzazione delle prestazioni può in alcuni casi condurre a un deadlock. Due attività potrebbero ad esempio eseguire lo stesso codice di delegato, che segnala quando si verifica un evento, quindi attende che l'altra attività segnali un evento. Se la seconda attività viene resa inline nello stesso thread del primo, e il primo entra in un ciclo di attesa, la seconda attività non sarà mai in grado di segnalare il rispettivo evento. Per evitare questa situazione, è possibile specificare un timeout sull'operazione di attesa o utilizzare costruttori di thread espliciti per garantire che un'attività non blocchi l'altra.
Non presupporre che le iterazioni di Foreach, For e ForAll vengano eseguite sempre in parallelo
È importante ricordare che le iterazioni singole in un ciclo For, ForEach o ForAll<TSource> possono essere eseguite in parallelo ma non è necessario che lo siano. È pertanto necessario evitare di scrivere codice la cui correttezza dipenda dall'esecuzione parallela delle iterazioni o dall'esecuzione delle iterazioni in un particolare ordine. Il codice seguente, ad esempio, è molto probabile che conduca a un deadlock:
Dim mres = New ManualResetEventSlim()
Enumerable.Range(0, Environment.ProcessorCount * 100) _
.AsParallel() _
.ForAll(Sub(j)
If j = Environment.ProcessorCount Then
Console.WriteLine("Set on {0} with value of {1}",
Thread.CurrentThread.ManagedThreadId, j)
mres.Set()
Else
Console.WriteLine("Waiting on {0} with value of {1}",
Thread.CurrentThread.ManagedThreadId, j)
mres.Wait()
End If
End Sub) ' deadlocks
ManualResetEventSlim mre = new ManualResetEventSlim();
Enumerable.Range(0, Environment.ProcessorCount * 100)
.AsParallel()
.ForAll((j) =>
{
if (j == Environment.ProcessorCount)
{
Console.WriteLine("Set on {0} with value of {1}",
Thread.CurrentThread.ManagedThreadId, j);
mre.Set();
}
else
{
Console.WriteLine("Waiting on {0} with value of {1}",
Thread.CurrentThread.ManagedThreadId, j);
mre.Wait();
}
}); //deadlocks
In questo esempio, un'unica iterazione imposta un evento e tutte le altre iterazioni attendono l'evento. Nessuna delle iterazioni in attesa può essere completata fino a quando non viene completata l'iterazione di impostazione dell'evento. È tuttavia possibile che le iterazioni in attesa blocchino tutti i thread utilizzati per eseguire il ciclo parallelo, prima che l'iterazione di impostazione dell'evento abbia avuto la possibilità di essere eseguita. Ciò comporta un deadlock. L'iterazione di impostazione dell'evento non verrà mai eseguita e le iterazioni in attesa non verranno mai riattivate.
In particolare, l'avanzamento di un'iterazione di un ciclo parallelo non deve dipendere da un'altra iterazione del ciclo. Se il ciclo parallelo decide di pianificare le iterazioni in sequenza ma nell'ordine opposto, si verificherà un deadlock.
Evitare di eseguire cicli paralleli sul thread UI
È importante mantenere reattiva l'interfaccia utente dell'applicazione. Se un'operazione comporta lavoro sufficiente a garantire la parallelizzazione, è probabile che non debba essere eseguita sul thread UI. Il carico di lavoro dell'operazione dovrebbe invece essere ripartito in modo che l'operazione venga eseguita su un thread in background. Se ad esempio si desidera utilizzare un ciclo parallelo per calcolare dati di cui deve essere eseguito il rendering in un controllo dell'interfaccia utente, è consigliabile eseguire il ciclo all'interno di un'istanza dell'attività anziché direttamente in un gestore eventi dell'interfaccia utente. Solo quando il calcolo principale è stato completato dovrebbe essere eseguito nuovamente il marshalling dell'aggiornamento dell'interfaccia utente nel thread UI.
Se si eseguono cicli paralleli sul thread UI, evitare di aggiornare controlli dell'interfaccia utente dall'interno del ciclo. Se si prova ad aggiornare controlli dell'interfaccia utente dall'interno di un ciclo parallelo in esecuzione sul thread UI possono verificarsi un danneggiamento dello stato, eccezioni, aggiornamenti ritardati e deadlock, a seconda di come viene richiamato l'aggiornamento dell'Interfaccia utente. Nell'esempio seguente il ciclo parallelo blocca il thread UI sul quale è in esecuzione fino a che non vengono completate tutte le iterazioni. Se tuttavia un'iterazione del ciclo è in esecuzione su un thread in background (come può accadere per For ), la chiamata al metodo Invoke comporta l'invio di un messaggio al thread UI e l'attesa da parte dei blocchi dell'elaborazione del messaggio. Poiché il thread UI è bloccato nell'esecuzione di For, il messaggio non potrà mai essere elaborato e si verifica un deadlock del thread UI.
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim iterations As Integer = 20
Parallel.For(0, iterations, Sub(x)
Button1.Invoke(Sub()
DisplayProgress(x)
End Sub)
End Sub)
End Sub
private void button1_Click(object sender, EventArgs e)
{
Parallel.For(0, N, i =>
{
// do work for i
button1.Invoke((Action)delegate { DisplayProgress(i); });
});
}
Nell'esempio seguente viene mostrato come evitare il deadlock mediante l'esecuzione del ciclo in un'istanza dell'attività. Il thread UI non è bloccato dal ciclo e il messaggio può essere elaborato.
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim iterations As Integer = 20
Task.Factory.StartNew(Sub() Parallel.For(0, iterations, Sub(x)
Button1.Invoke(Sub()
DisplayProgress(x)
End Sub)
End Sub))
End Sub
private void button1_Click(object sender, EventArgs e)
{
Task.Factory.StartNew(() =>
Parallel.For(0, N, i =>
{
// do work for i
button1.Invoke((Action)delegate { DisplayProgress(i); });
})
);
}
Vedere anche
Concetti
Programmazione parallela in .NET Framework
Problemi potenziali dell'utilizzo di PLINQ