Problemi potenziali dell'utilizzo di PLINQ

In molti casi, PLINQ può fornire miglioramenti significativi a livello di prestazioni su query LINQ to Objects sequenziali. Le operazioni necessarie per parallelizzare l'esecuzione delle query 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 query PLINQ.

Non presupporre che l'approccio in parallelo sia sempre più veloce

In alcuni casi la parallelizzazione rallenta l'esecuzione di una query PLINQ rispetto all'esecuzione di una query LINQ to Objects equivalente. La regola generale di base è che per le query con pochi elementi di origine 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 prima di decidere se usare PLINQ. Per altre informazioni, vedere Informazioni sull'aumento di velocità in PLINQ.

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 una query PLINQ.

Evitare parallelizzazioni eccessive

L'uso del metodo AsParallel comporta costi di sovraccarico dovuti al partizionamento della raccolta 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 una query.

La situazione più comune in cui si verifica la parallelizzazione eccessiva è quando si usano query annidate, come illustrato nel frammento di codice seguente.

var q = from cust in customers.AsParallel()
        from order in cust.Orders.AsParallel()
        where order.OrderDate > date
        select new { cust, order };
Dim q = From cust In customers.AsParallel()
        From order In cust.Orders.AsParallel()
        Where order.OrderDate > aDate
        Select New With {cust, order}

In questo caso è meglio parallelizzare solo l'origine dati esterna (clienti), a meno che non sussista almeno una delle condizioni seguenti:

  • È noto che l'origine dati interna (cust.Orders) è molto lunga.

  • 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 misure. Per altre informazioni, vedere Procedura: Misurare le prestazioni di esecuzione delle query di PLINQ.

Evitare chiamate a metodi non thread-safe

La scrittura in metodi di istanza non thread-safe da una query PLINQ 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.Write. Tuttavia, la classe non supporta le chiamate simultanee.

Dim fs As FileStream = File.OpenWrite(…)
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(Sub(x) fs.Write(x))
FileStream fs = File.OpenWrite(...);
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(x => fs.Write(x));

Limitare le chiamate ai metodi thread-safe

La maggior parte dei metodi statici in .NET è 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 usato a scopo dimostrativo negli esempi della documentazione, è consigliabile evitare di usarlo nelle query PLINQ.

Evitare operazioni di ordinamento non necessarie

Quando PLINQ esegue una query in parallelo, divide la sequenza di origine in partizioni che possono essere usate contemporaneamente su più thread. Per impostazione predefinita, l'ordine di elaborazione delle partizioni e i risultati restituiti non sono prevedibili, ad eccezione degli operatori quali, ad esempio, OrderBy. È possibile indicare a PLINQ di conservare l'ordine di qualsiasi sequenza di origine, ma ciò ha un impatto negativo sulle prestazioni. La procedura consigliata, se possibile, è strutturare le query in modo che non si basino sulla conservazione dell'ordine. Per altre informazioni, vedere Conservazione dell'ordine in PLINQ.

Preferire ForAll a ForEach quando è possibile

Anche se PLINQ esegue una query su più thread, se si usano i risultati in un ciclo foreach (For Each in Visual Basic), i risultati della query devono essere uniti in un unico thread e l'enumeratore dovrà accedervi in modo seriale. In alcuni casi, questo è inevitabile. Tuttavia, se possibile, usare il metodo ForAll per consentire a ogni thread di restituire i propri risultati, ad esempio scrivendo in una raccolta thread-safe quale System.Collections.Concurrent.ConcurrentBag<T>.

Lo stesso problema si applica a Parallel.ForEach. In altre parole, source.AsParallel().Where().ForAll(...) deve essere fortemente preferito a Parallel.ForEach(source.AsParallel().Where(), ...).

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. Ad esempio, sia in Windows Form sia in WPF, l'accesso a un controllo può essere eseguito solo nel thread in cui è stato creato. Se si tenta di accedere allo stato condiviso di un controllo Windows Form in una query PLINQ, viene generata un'eccezione se si esegue il debugger. Questa impostazione può essere disattivata. Tuttavia, se la query viene usata nel thread dell'interfaccia utente, è possibile accedere al controllo dal ciclo foreach che enumera i risultati della query perché tale codice viene eseguito in un solo thread.

Non presupporre che le iterazioni di Foreach, For e ForAll vengano eseguite sempre in parallelo

È importante tenere presente che le singole iterazioni in un ciclo Parallel.For, Parallel.ForEach o ForAll possono non necessariamente essere eseguite in parallelo. È 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 mre = 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)
       mre.Set()
   Else
       Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
       mre.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.

Vedi anche