Introduzione a PLINQ
Definizione di query parallela
LINQ (Language Integrated Query), introdotto nella versione 3.0 di .NET Framework, offre un modello unificato per l'esecuzione di query su qualsiasi origine dati System.Collections.IEnumerable o System.Collections.Generic.IEnumerable<T> in modo indipendente dai tipi. LINQ to Objects è il nome dato alle query LINQ eseguite su insiemi in memoria quali gli oggetti List<T> e le matrici. Questo articolo presuppone che si conoscano le nozioni di base di LINQ. Per ulteriori informazioni, vedere LINQ (Language-Integrated Query).
Parallel LINQ (PLINQ) è un'implementazione in parallelo del modello LINQ. Sotto molti aspetti una query PLINQ assomiglia a una query LINQ to Objects non parallela. Le query PLINQ, analogamente alle query LINQ sequenziali, funzionano su qualsiasi origine dati in memoria IEnumerable o IEnumerable<T>. Vengono inoltre eseguite in modo posticipato, ovvero solo dopo l'enumerazione della query. La differenza principale è 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, quando si utilizza l'esecuzione parallela la query viene eseguita in tempi significativamente minori.
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 punti, quale l'ordinamento, influiscono sulle query parallele. Per ulteriori informazioni, vedere Informazioni sull'aumento di velocità in PLINQ.
![]() |
---|
Questa documentazione utilizza espressioni lambda per definire delegati in PLINQ.Se non si ha familiarità con le espressioni lambda in C# o Visual Basic, vedere Espressioni lambda in PLINQ e TPL. |
Nella parte restante di questo articolo vengono fornite informazioni generali sulle classi PLINQ principali. Inoltre, viene descritto come creare query PLINQ. Ogni sezione contiene collegamenti a informazioni ed esempi di codice più dettagliati.
Classe ParallelEnumerable
La classe System.Linq.ParallelEnumerable espone quasi tutte le funzionalità di PLINQ. Tale classe e il resto dei tipi dello spazio dei nomi System.Linq vengono compilati nell'assembly System.Core.dll. In Visual Studio entrambi i progetti C# e Visual Basic predefiniti fanno riferimento all'assembly e importano lo spazio dei nomi.
L'oggetto ParallelEnumerable include le implementazioni di tutti gli operatori di 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.
Oltre agli operatori di 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 |
---|---|
Punto di ingresso di PLINQ. Specifica che il resto della query deve essere parallelizzato, se è possibile. |
|
Specifica che il resto della query deve essere eseguito in sequenza, come una query LINQ non parallela. |
|
Specifica che PLINQ deve conservare l'ordine della sequenza di origine per il resto della query oppure finché non venga modificato l'ordine, ad esempio tramite la clausola orderby (Order By in Vlsual Basic). |
|
Specifica che non è necessario che PLINQ conservi l'ordine della sequenza di origine per il resto della query. |
|
Specifica che PLINQ deve monitorare periodicamente lo stato del token di annullamento fornito e, se richiesto, annullare l'esecuzione. |
|
Specifica il numero massimo di processori che PLINQ deve utilizzare per parallelizzare la query. |
|
Fornisce un suggerimento su come PLINQ deve unire i risultati paralleli in un'unica sequenza nel thread consumer qualora tale unione risulti possibile. |
|
Specifica se PLINQ deve parallelizzare la query anche quando il comportamento predefinito ne prevede l'esecuzione sequenziale. |
|
Metodo di enumerazione multithreading che, a differenza dello scorrimento dei risultati della query, consente ai risultati di essere elaborati in parallelo senza prima essere uniti nel thread consumer. |
|
Overload di 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 sull'origine dati, come mostrato nell'esempio seguente.
Dim source = Enumerable.Range(1, 10000)
' Opt in to PLINQ with AsParallel
Dim evenNums = From num In source.AsParallel()
Where Compute(num) > 0
Select num
var source = Enumerable.Range(1, 10000);
// Opt-in to PLINQ with AsParallel
var evenNums = from num in source.AsParallel()
where Compute(num) > 0
select num;
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. In fase di esecuzione, 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 non è sicuro parallelizzare una query, PLINQ si limita ad eseguire la query in modo sequenziale. Se occorre scegliere fra un algoritmo in parallelo potenzialmente dispendioso e un algoritmo sequenziale poco dispendioso, per impostazione predefinita PLINQ sceglie l'algoritmo sequenziale. È possibile utilizzare il metodo WithExecutionMode<TSource> e l'enumerazione System.Linq.ParallelExecutionMode per indicare a PLINQ di scegliere l'algoritmo in parallelo. Ciò si rivela utile quando si rileva tramite test e misurazioni che una determinata query risulta più veloce quando viene eseguita in parallelo. Per ulteriori informazioni, vedere Procedura: specificare la modalità di esecuzione in PLINQ.
Livello di parallelismo
Per impostazione predefinita, PLINQ utilizza tutti i processori nel computer host fino a un massimo di 64. Tramite il metodo WithDegreeOfParallelism<TSource> è possibile indicare a PLINQ di utilizzare al massimo un numero specificato di processori. Ciò si rivela utile quando si desidera garantire che gli altri processi in esecuzione nel computer ricevano una determinata quantità di tempo CPU. Nel frammento seguente si consente alla query di utilizzare al massimo due processori.
Dim query = From item In source.AsParallel().WithDegreeOfParallelism(2)
Where Compute(item) > 42
Select item
var 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 livello di parallelismo maggiore del numero di core nel computer.
Confronto fra query parallele ordinate e non ordinate
In alcune query, un operatore di query deve produrre risultati che conservino l'ordine della sequenza di origine. A tale scopo, PLINQ fornisce l'operatore AsOrdered. AsOrdered è diverso da AsSequential<TSource>. Una sequenza AsOrdered viene comunque elaborata in parallelo, ma i relativi 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<TSource> predefinita. La possibilità che una determinata operazione in parallelo ordinata risulti più veloce di una versione sequenziale dell'operazione dipende da molti fattori.
Nell'esempio di codice seguente viene mostrato come scegliere esplicitamente la conservazione dell'ordine.
Dim evenNums = From num In numbers.AsParallel().AsOrdered()
Where num Mod 2 = 0
Select num
evenNums = from num in numbers.AsParallel().AsOrdered()
where num % 2 == 0
select num;
Per ulteriori informazioni, vedere Conservazione dell'ordine in PLINQ.
Confronto fraquery parallele e sequenziali
Alcune operazioni richiedono che i dati di origine vengano recapitati in modo sequenziale. Quando occorre, 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<TSource>. Quando si utilizza AsSequential<TSource>, tutti gli operatori successivi nella query vengono eseguiti in modo sequenziale finché AsParallel non viene chiamato nuovamente. Per ulteriori informazioni, vedere Procedura: combinare query LINQ parallele e sequenziali.
Opzioni relative all'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 utilizzati 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 cominciare a produrre risultati più velocemente. A tale scopo, PLINQ supporta il metodo WithMergeOptions<TSource> e l'enumerazione ParallelMergeOptions. Per ulteriori informazioni, vedere Opzioni di unione in PLINQ.
Operatore ForAll
Nelle query di LINQ sequenziali l'esecuzione viene posticipata finché la query non viene enumerata in un ciclo foreach (For Each in Visual Basic) o tramite la chiamata di un metodo quale ToList<TSource>, ToTSource> o ToDictionary. In PLINQ è anche possibile utilizzare foreach per eseguire la query e scorrere i risultati. Tuttavia, poiché di per sé 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 utilizzare 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, utilizzare il metodo ForAll<TSource> per eseguire una query PLINQ. ForAll<TSource> non esegue questo passaggio di unione finale. Nell'esempio di codice seguente viene illustrato l'utilizzo del metodo ForAll<TSource>. Viene qui utilizzato System.Collections.Concurrent.ConcurrentBag<T> poiché è ottimizzato per l'aggiunta simultanea di più thread senza il tentativo di rimuovere elementi.
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)))
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)));
Nell'illustrazione seguente viene illustrata la differenza tra foreach e ForAll<TSource> per quanto riguarda l'esecuzione di una query.
Annullamento
PLINQ è integrato con i tipi di annullamento in .NET Framework 4. Per ulteriori informazioni, vedere Annullamento. Pertanto, a differenza delle query LINQ to Objects sequenziali, le query PLINQ possono essere annullate. Per creare una query PLINQ annullabile, utilizzare l'operatore WithCancellation<TSource> nella query e fornire un'istanza CancellationToken come argomento. Quando la proprietà IsCancellationRequested nel token viene impostata su true, PLINQ lo rileverà, interromperà l'elaborazione in tutti i thread e genererà un oggetto 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 ulteriori informazioni, vedere Procedura: annullare una query PLINQ.
Eccezioni
Quando una query PLINQ viene eseguita è 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 utilizza 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. Tuttavia è possibile scorrere tutte le eccezioni incapsulate in AggregateException e rilevare tutte quelle gestibili in modo sicuro. In casi rari, alcune eccezioni possono essere generate senza wrapping in un oggetto AggregateException. Inoltre, gli oggetti ThreadAbortException non prevedono 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 ulteriori 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 stesso è l'oggetto enumerabile su cui viene eseguita la query.
[Visual Basic]
Dim arr(10000) As Integer
Dim partitioner = New MyArrayPartitioner(Of Integer)(arr)
Dim query = partitioner.AsParallel().Select(Function(x) SomeFunction(x))
[C#]
int[] arr= ...;
Partitioner<int> partitioner = newMyArrayPartitioner<int>(arr);
var q = partitioner.AsParallel().Select(x => SomeFunction(x));
PLINQ supporta un numero fisso di partizioni. È tuttavia possibile che in fase di esecuzione i dati vengano riassegnati dinamicamente a tali 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 ulteriori informazioni, vedere Partitioner personalizzati per PLINQ e TPL.
Misura 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 una query non esegue molto calcolo o se l'origine dati è di dimensioni ridotte, è possibile che una query PLINQ sia più lenta di una query LINQ to Objects sequenziale. È possibile utilizzare Parallel Performance Analyzer in Visual Studio Team Server per confrontare le prestazioni di varie query, per individuare colli di bottiglia di elaborazione e determinare se l'esecuzione di una query è in parallelo o sequenziale. Per ulteriori informazioni, vedere Visualizzatore di concorrenze e Procedura: misurare le prestazioni di esecuzione delle query di PLINQ.