Introduzione a PLINQ

LINQ parallelo (PLINQ) è un'implementazione parallela del modello LINQ (Language-Integrated Query). PLINQ implementa il set completo di operatori di query standard LINQ come metodi di estensione per lo spazio dei nomi System.Linq e dispone di operatori aggiuntivi per le operazioni parallele. PLINQ combina la semplicità e la leggibilità della sintassi di LINQ con la potenza di programmazione in parallelo.

Suggerimento

Se non si ha familiarità con LINQ, esso offre un modello unificato per l'esecuzione di query su qualsiasi origine dati enumerabile in modo indipendente dai tipi. LINQ to Objects è il nome delle query LINQ eseguite su raccolte in memoria, ad esempio oggetti List<T> e matrici. Questo articolo presuppone che si abbia già una conoscenza delle nozioni di base di LINQ. Per altre informazioni, vedere Language Integrated Query (LINQ).

Che cos'è una query parallela?

Una query PLINQ è molto simile a una query LINQ to Objects non parallela. Analogamente alle query LINQ sequenziali, le query PLINQ possono essere eseguite su qualsiasi origine dati IEnumerable o IEnumerable<T> in memoria e hanno un'esecuzione posticipata, ovvero l'esecuzione viene avviata solo dopo l'enumerazione della query. La differenza principale consiste nel fatto che PLINQ tenta di sfruttare al massimo tutti i processori del sistema. A tale scopo esegue il partizionamento dell'origine dati in segmenti e quindi esegue la query su ogni segmento in thread di lavoro distinti e in parallelo su più processori. In molti casi, l'esecuzione parallela rende notevolmente più rapida l'esecuzione della query.

Tramite l'esecuzione parallela, PLINQ può garantire per determinati tipi di query prestazioni significativamente migliori rispetto al codice legacy, spesso con la sola aggiunta dell'operazione di query AsParallel all'origine dati. Tuttavia, il parallelismo può introdurre complessità intrinseche e non tutte le operazioni di query presentano un'esecuzione più veloce in PLINQ. Di fatto, per determinate query la parallelizzazione comporta un'esecuzione più lenta. È pertanto necessario comprendere il modo in cui alcuni aspetti, ad esempio l'ordinamento, influiscono sulle query parallele. Per altre informazioni, vedere Informazioni sull'aumento di velocità in PLINQ.

Nota

Le espressioni lambda sono usate nella documentazione per definire i delegati in PLINQ. Se non si ha familiarità con le espressioni lambda in C# o Visual Basic, vedere Espressioni lambda in PLINQ e TPL.

La parte rimanente di questo articolo offre una panoramica delle classi PLINQ principali e descrive come creare query PLINQ. Ogni sezione contiene collegamenti a informazioni dettagliate ed esempi di codice.

Classe ParallelEnumerable

La classe System.Linq.ParallelEnumerable espone quasi tutte le funzionalità di PLINQ. Questa classe e gli altri tipi dello spazio dei nomi System.Linq vengono compilati nell'assembly System.Core.dll. In Visual Studio i progetti C# e Visual Basic predefiniti fanno riferimento all'assembly e importano lo spazio dei nomi.

ParallelEnumerable include le implementazioni di tutti gli operatori query standard supportati da LINQ to Objects, anche se non tenta di parallelizzare ognuno di essi. Se non si ha familiarità con LINQ, vedere Introduzione a LINQ (C#) e Introduzione a LINQ (Visual Basic).

Oltre agli operatori query standard, la classe ParallelEnumerable contiene un set di metodi che rendono possibili comportamenti specifici dell'esecuzione parallela. Questi metodi specifici di PLINQ sono elencati nella tabella seguente.

Operatore ParallelEnumerable Descrizione
AsParallel Punto di ingresso di PLINQ. Specifica che la parte rimanente della query deve essere parallelizzata, se è possibile.
AsSequential Specifica che la parte rimanente della query deve essere eseguita in sequenza, come una query LINQ non parallela.
AsOrdered Specifica che PLINQ deve conservare l'ordine della sequenza di origine per la parte rimanente della query oppure fino a quando l'ordine non viene modificato, ad esempio tramite la clausola orderby (Order By in Visual Basic).
AsUnordered Specifica che non è necessario che PLINQ conservi l'ordine della sequenza di origine per la parte rimanente della query.
WithCancellation Specifica che PLINQ deve monitorare periodicamente lo stato del token di annullamento specificato e, se richiesto, annullare l'esecuzione.
WithDegreeOfParallelism Specifica il numero massimo di processori che PLINQ deve usare per parallelizzare la query.
WithMergeOptions Offre un suggerimento su come PLINQ deve unire, se possibile, i risultati paralleli in un'unica sequenza nel thread in uso.
WithExecutionMode Specifica se PLINQ deve parallelizzare la query anche quando il comportamento predefinito ne prevede l'esecuzione sequenziale.
ForAll Metodo di enumerazione multithreading che, a differenza dell'iterazione sui risultati della query, consente ai risultati di essere elaborati in parallelo senza prima essere uniti nel thread in uso.
Overload Aggregate Overload esclusivo di PLINQ che consente l'aggregazione intermedia su partizioni di thread locali e che offre una funzione di aggregazione finale per combinare i risultati di tutte le partizioni.

Modello basato su scelta esplicita

Quando si scrive una query è possibile scegliere esplicitamente PLINQ richiamando il metodo di estensione ParallelEnumerable.AsParallel nell'origine dati, come illustrato nell'esempio seguente.

var source = Enumerable.Range(1, 10000);

// Opt in to PLINQ with AsParallel.
var evenNums = from num in source.AsParallel()
               where num % 2 == 0
               select num;
Console.WriteLine("{0} even numbers out of {1} total",
                  evenNums.Count(), source.Count());
// The example displays the following output:
//       5000 even numbers out of 10000 total
Dim source = Enumerable.Range(1, 10000)

' Opt in to PLINQ with AsParallel
Dim evenNums = From num In source.AsParallel()
               Where num Mod 2 = 0
               Select num
Console.WriteLine("{0} even numbers out of {1} total",
                  evenNums.Count(), source.Count())
' The example displays the following output:
'       5000 even numbers out of 10000 total

Il metodo di estensione AsParallel associa gli operatori di query successivi, in questo caso where e select, alle implementazioni di System.Linq.ParallelEnumerable.

Modalità di esecuzione

Per impostazione predefinita, PLINQ è conservativo. Durante il runtime, l'infrastruttura PLINQ analizza la struttura complessiva della query. Se è probabile che la parallelizzazione della query comporti una maggiore velocità di esecuzione, PLINQ esegue il partizionamento della sequenza di origine in attività eseguibili simultaneamente. Se la parallelizzazione della query non risulta un'operazione sicura, PLINQ si limita ad eseguire la query in modo sequenziale. Tra un algoritmo in parallelo potenzialmente dispendioso e un algoritmo sequenziale poco dispendioso, per impostazione predefinita PLINQ sceglie l'algoritmo sequenziale. È possibile usare il metodo WithExecutionMode e l'enumerazione System.Linq.ParallelExecutionMode per indicare a PLINQ di selezionare l'algoritmo parallelo. Ciò è utile quando si scopre tramite test e misurazioni che una determinata query risulta più veloce quando viene eseguita in parallelo. Per altre informazioni, vedere Procedura: Specificare la modalità di esecuzione in PLINQ.

Grado di parallelismo

Per impostazione predefinita, PLINQ usa tutti i processori nel computer host. Usando il metodo WithDegreeOfParallelism è possibile indicare a PLINQ di usare un numero massimo di processori specificato. Ciò è utile quando si vuole garantire che gli altri processi in esecuzione nel computer ricevano una determinata quantità di tempo CPU. Il frammento seguente consente alla query di usare al massimo due processori.

var query = from item in source.AsParallel().WithDegreeOfParallelism(2)
            where Compute(item) > 42
            select item;
Dim query = From item In source.AsParallel().WithDegreeOfParallelism(2)
            Where Compute(item) > 42
            Select item

Nei casi in cui una query stia eseguendo una quantità significativa di lavoro privo di vincoli di calcolo, ad esempio l'I/O dei file, può risultare vantaggioso specificare un grado di parallelismo maggiore del numero di core nel computer.

Confronto fra query parallele ordinate e non ordinate

In alcune query l'operatore di query deve produrre risultati che conservano l'ordine della sequenza di origine. A questo scopo, PLINQ fornisce l'operatore AsOrdered. AsOrdered si differenzia da AsSequential. Una sequenza AsOrdered viene comunque elaborata in parallelo, ma i risultati vengono memorizzati nel buffer e ordinati. Poiché la conservazione dell'ordine comporta in genere un lavoro aggiuntivo, è possibile che una sequenza AsOrdered venga elaborata più lentamente rispetto alla sequenza AsUnordered predefinita. La possibilità che una determinata operazione in parallelo ordinata risulti più veloce di una versione sequenziale dell'operazione dipende da molti fattori.

L'esempio di codice seguente illustra come scegliere esplicitamente la conservazione dell'ordine.

var evenNums =
    from num in numbers.AsParallel().AsOrdered()
    where num % 2 == 0
    select num;
Dim evenNums = From num In numbers.AsParallel().AsOrdered()
               Where num Mod 2 = 0
               Select num


Per altre informazioni, vedere Conservazione dell'ordine in PLINQ.

Confronto fra query parallele e sequenziali

Alcune operazioni richiedono che i dati di origine vengano recapitati in modo sequenziale. Quando è necessario, gli operatori di query ParallelEnumerable ripristinano automaticamente la modalità sequenziale. Per gli operatori di query definiti dall'utente e i delegati dell'utente che richiedono l'esecuzione sequenziale, PLINQ fornisce il metodo AsSequential. Quando si usa AsSequential, tutti gli operatori successivi nella query vengono eseguiti in sequenza fino a quando non viene richiamato il metodo AsParallel. Per altre informazioni, vedere Procedura: Combinare query LINQ parallele e sequenziali.

Opzioni di unione dei risultati di query

Quando una query PLINQ viene eseguita in parallelo, i relativi risultati ottenuti da ogni thread di lavoro devono essere uniti nel thread principale affinché vengano usati da un ciclo foreach (For Each in Visual Basic) o vengano inseriti in un elenco o in una matrice. In alcuni casi può essere vantaggioso specificare un determinato tipo di operazione di unione, ad esempio per iniziare a produrre risultati più velocemente. A tale scopo, PLINQ supporta il metodo WithMergeOptions e l'enumerazione ParallelMergeOptions. Per altre informazioni, vedere Opzioni di unione in PLINQ.

Operatore ForAll

Nelle query LINQ sequenziali l'esecuzione viene posticipata fino a quando la query non viene enumerata in un ciclo foreach (For Each in Visual Basic) o richiamando un metodo come ToList, ToArray o ToDictionary. In PLINQ è anche possibile usare foreach per eseguire la query e l'iterazione dei risultati. Tuttavia, poiché non viene eseguito in parallelo, foreach richiede che l'output di tutte le attività in parallelo venga unito di nuovo nel thread in cui il ciclo è in esecuzione. In PLINQ è possibile usare foreach quando è necessario conservare l'ordine finale dei risultati della query nonché quando si elaborano i risultati in modo seriale, ad esempio quando si chiama Console.WriteLine per ogni elemento. Per eseguire più velocemente una query quando la conservazione dell'ordine non è richiesta e quando la stessa elaborazione dei risultati può essere parallelizzata, usare il metodo ForAll per eseguire una query PLINQ. ForAll non esegue questo passaggio di merge finale. Nell'esempio di codice riportato di seguito viene illustrato come utilizzare il metodo ForAll. In questo caso viene usato l'oggetto System.Collections.Concurrent.ConcurrentBag<T> perché è ottimizzato per l'aggiunta simultanea di più thread senza un tentativo di rimuovere gli elementi.

var nums = Enumerable.Range(10, 10000);
var query =
    from num in nums.AsParallel()
    where num % 10 == 0
    select num;

// Process the results as each thread completes
// and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
// which can safely accept concurrent add operations
query.ForAll(e => concurrentBag.Add(Compute(e)));
Dim nums = Enumerable.Range(10, 10000)
Dim query = From num In nums.AsParallel()
            Where num Mod 10 = 0
            Select num

' Process the results as each thread completes
' and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
' which can safely accept concurrent add operations
query.ForAll(Sub(e) concurrentBag.Add(Compute(e)))

La figura seguente mostra la differenza tra foreach e ForAll per quanto riguarda l'esecuzione di query.

ForAll vs. ForEach

Annullamento

PLINQ è integrato con i tipi di annullamento in .NET. Per altre informazioni, vedere Annullamento in thread gestiti. Di conseguenza, a differenza delle query LINQ to Objects sequenziali, le query PLINQ possono essere annullate. Per creare una query PLINQ annullabile, usare l'operatore WithCancellation nella query e fornire un'istanza di CancellationToken come argomento. Quando la proprietà IsCancellationRequested nel token è impostata su true, PLINQ rileva questa impostazione, arresta l'elaborazione in tutti i thread e genera un'eccezione OperationCanceledException.

È possibile che una query PLINQ continui a elaborare alcuni elementi dopo l'impostazione del token di annullamento.

Per garantire una maggiore capacità di risposta, è possibile rispondere alle richieste di annullamento anche nei delegati dell'utente di lunga durata. Per altre informazioni, vedere Procedura: Annullare una query PLINQ.

Eccezioni

Quando viene eseguita una query PLINQ, è possibile che vengano generate simultaneamente più eccezioni da thread diversi. Inoltre, il codice per gestire l'eccezione e quello che ha generato l'eccezione potrebbero trovarsi in thread diversi. PLINQ usa il tipo AggregateException per incapsulare tutte le eccezioni generate da una query e quindi eseguirne il marshalling nel thread chiamante. Nel thread chiamante è necessario un unico blocco try-catch. È possibile tuttavia eseguire l'iterazione in tutte le eccezioni incapsulate in AggregateException e rilevare quelle gestibili in modo sicuro. In casi rari, alcune eccezioni possono essere generate senza wrapping in AggregateException. Anche ThreadAbortException non prevede il wrapping.

Quando alle eccezioni è consentita la propagazione fino al thread di unione, è possibile che una query continui a elaborare alcuni elementi dopo la generazione dell'eccezione.

Per altre informazioni, vedere Procedura: Gestire le eccezioni in una query PLINQ.

Partitioner personalizzati

In alcuni casi è possibile migliorare le prestazioni delle query scrivendo un Partitioner personalizzato che sfrutta alcune caratteristiche dei dati di origine. Nella query il partitioner personalizzato è l'oggetto enumerabile su cui viene eseguita la query.

int[] arr = new int[9999];
Partitioner<int> partitioner = new MyArrayPartitioner<int>(arr);
var query = partitioner.AsParallel().Select(SomeFunction);
Dim arr(10000) As Integer
Dim partitioner As Partitioner(Of Integer) = New MyArrayPartitioner(Of Integer)(arr)
Dim query = partitioner.AsParallel().Select(Function(x) SomeFunction(x))

PLINQ supporta un numero fisso di partizioni. È tuttavia possibile che durante il runtime i dati vengano riassegnati dinamicamente alle partizioni per il bilanciamento del carico. For e ForEach supportano solo il partizionamento dinamico, il che significa che il numero di partizioni cambia in fase di esecuzione. Per altre informazioni, vedere Partitioner personalizzati per PLINQ e TPL.

Misurazione delle prestazioni di PLINQ

In molti casi una query può essere parallelizzata, ma le risorse necessarie per configurare la query parallela rappresentano uno svantaggio superiore al vantaggio ottenuto in termini di prestazioni. Se la query esegue pochi calcoli o se l'origine dati è di dimensioni ridotte, è possibile che una query PLINQ risulti più lenta di una query LINQ to Objects sequenziale. È possibile usare Parallel Performance Analyzer in Visual Studio Team Server per confrontare le prestazioni delle diverse query per individuare colli di bottiglia di elaborazione e determinare se la query è eseguita in parallelo o in modo sequenziale. Per altre informazioni, vedere Visualizzatore di concorrenze e Procedura: Misurare le prestazioni di esecuzione delle query di PLINQ.

Vedi anche