Condividi tramite


Potenziali insidie con PLINQ

In molti casi, PLINQ può offrire miglioramenti significativi delle prestazioni rispetto alle query LINQ to Objects sequenziali. Tuttavia, il lavoro di parallelizzazione dell'esecuzione della query introduce complessità che possono causare problemi che, nel codice sequenziale, non sono così comuni o non vengono rilevati affatto. In questo argomento vengono elencate alcune procedure da evitare quando si scrivono query PLINQ.

Non presupporre che il parallelismo sia sempre più veloce

La parallelizzazione a volte causa l'esecuzione di una query PLINQ più lenta rispetto all'equivalente LINQ to Objects. La regola base è che è improbabile che le query con pochi elementi di origine e delegati utente veloci possano essere accelerate. Tuttavia, poiché molti fattori sono coinvolti nelle prestazioni, è consigliabile misurare i risultati effettivi prima di decidere se usare PLINQ. Per ulteriori dettagli, consulta Comprendere l'accelerazione in PLINQ.

Evitare di scrivere in locazioni di memoria condivise

Nel codice sequenziale non è insolito leggere o scrivere in variabili statiche o campi di classe. Tuttavia, ogni volta che più thread accedono contemporaneamente a tali variabili, esiste un grande potenziale per le race condition. Anche se è possibile usare blocchi per sincronizzare l'accesso alla variabile, il costo della sincronizzazione può compromettere le prestazioni. È pertanto consigliabile evitare, o almeno limitare, l'accesso allo stato condiviso in una query PLINQ il più possibile.

Evitare la parallelizzazione eccessiva

Usando il AsParallel metodo , si comportano i costi generali per il partizionamento della raccolta di origine e la sincronizzazione dei thread di lavoro. I vantaggi della parallelizzazione sono ulteriormente limitati dal numero di processori nel computer. Non vi è alcuna velocità da ottenere eseguendo più thread associati a calcolo su un solo processore. Pertanto, è necessario prestare attenzione a non sovra parallelizzare una query.

Lo scenario più comune in cui la parallelizzazione eccessiva può verificarsi è nelle 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, è consigliabile parallelizzare solo l'origine dati esterna (clienti) a meno che non si applichino una o più delle condizioni seguenti:

  • L'origine dati interna (cust.Orders) è nota per essere molto lunga.

  • Si sta eseguendo un calcolo costoso per ogni ordine. L'operazione illustrata nell'esempio non è costosa.

  • Il sistema di destinazione è noto per avere processori sufficienti per gestire il numero di thread che verranno prodotti parallelizzando la query in cust.Orders.

In tutti i casi, il modo migliore per determinare la forma ottimale della query consiste nel testare e misurare. Per altre informazioni, vedere Procedura: Misurare le prestazioni delle query PLINQ.

Evitare chiamate a metodi non thread-safe

La scrittura in metodi di istanza non thread-safe da una query PLINQ può causare un danneggiamento dei dati che potrebbe o non essere rilevato nel programma. Può anche causare eccezioni. Nell'esempio seguente più thread tentano di chiamare contemporaneamente il FileStream.Write metodo , che non è supportato dalla classe .

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 da più thread contemporaneamente. Tuttavia, anche in questi casi, la sincronizzazione interessata può causare un rallentamento significativo nella query.

Annotazioni

È possibile testarlo manualmente inserendo alcune chiamate a WriteLine nelle query. Anche se questo metodo viene usato negli esempi di documentazione a scopo dimostrativo, non 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 gestite simultaneamente su più thread. Per impostazione predefinita, l'ordine in cui vengono elaborate le partizioni e i risultati vengono recapitati non è prevedibile (ad eccezione di operatori come OrderBy). È possibile indicare a PLINQ di mantenere l'ordinamento di qualsiasi sequenza di origine, ma questo ha un impatto negativo sulle prestazioni. La procedura consigliata, quando possibile, consiste nel strutturare le query in modo che non si basi sul mantenimento dell'ordine. Per altre informazioni, vedere Conservazione degli ordini in PLINQ.

Preferire ForAll a ForEach quando è possibile

Anche se PLINQ esegue una query su più thread, se si utilizzano i risultati in un foreach ciclo (For Each in Visual Basic), i risultati della query devono essere uniti di nuovo in un thread e accessibili serialmente dall'enumeratore. In alcuni casi, questo è inevitabile; tuttavia, quando possibile, utilizzare il metodo ForAll per consentire a ogni thread di produrre i propri risultati, ad esempio scrivendo in una raccolta thread-safe come System.Collections.Concurrent.ConcurrentBag<T>.

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

Tenere presente i problemi di affinità dei thread

Alcune tecnologie, ad esempio, l'interoperabilità COM per componenti Single-Threaded Apartment (STA), Windows Form e Windows Presentation Foundation (WPF), impongono restrizioni di affinità thread che richiedono l'esecuzione del codice in un thread specifico. Ad esempio, sia in Windows Form che in WPF, è possibile accedere a un controllo solo nel thread in cui è stato creato. Se si tenta di accedere allo stato condiviso di un controllo Windows Forms in una query PLINQ, si verifica un'eccezione se si sta eseguendo il debugger. Questa impostazione può essere disattivata. Tuttavia, se la query viene utilizzata nel thread dell'interfaccia utente, è possibile accedere al controllo dal foreach ciclo che enumera i risultati della query perché tale codice viene eseguito su un solo thread.

Non presupporre che le iterazioni di ForEach, For e ForAll vengono sempre eseguite in parallelo

È importante tenere presente che le singole iterazioni in un Parallel.For, Parallel.ForEach o ForAll ciclo possono, ma non devono necessariamente, essere eseguite in parallelo. Pertanto, è consigliabile evitare di scrivere codice che dipende dalla correttezza dell'esecuzione parallela di iterazioni o dall'esecuzione di iterazioni in qualsiasi ordine specifico.

Ad esempio, questo codice rischia di andare in stallo.

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'iterazione imposta un evento e tutte le altre iterazioni attendono l'evento. Nessuna delle iterazioni in attesa può essere completata fino al completamento dell'iterazione che imposta l'evento. Tuttavia, è possibile che le iterazioni in attesa blocchino tutti i thread usati per eseguire il ciclo parallelo, prima che l'iterazione dell'impostazione dell'evento abbia avuto la possibilità di eseguire. Ciò comporta un deadlock: l'iterazione dell'impostazione dell'evento non verrà mai eseguita e le iterazioni in attesa non si riattivano mai.

In particolare, un'iterazione di un ciclo parallelo non deve mai attendere un'altra iterazione del ciclo per fare progressi. Se il ciclo parallelo decide di pianificare le iterazioni in sequenza ma nell'ordine opposto, si verificherà un deadlock.

Vedere anche