Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Parallel LINQ (PLINQ) è un'implementazione parallela del modello di query LINQ (Language-Integrated). PLINQ implementa il set completo di operatori di query standard LINQ come metodi di estensione per lo System.Linq spazio dei nomi e dispone di operatori aggiuntivi per le operazioni parallele. PLINQ combina la semplicità e la leggibilità della sintassi LINQ con la potenza della programmazione parallela.
Suggerimento
Se non si è familiari con LINQ, presenta un modello unificato per eseguire query su qualsiasi origine dati enumerabile in modo sicuro rispetto ai tipi. LINQ to Objects è il nome delle query LINQ eseguite su raccolte in memoria, come ad esempio List<T> e array. Questo articolo presuppone che si abbia una conoscenza di base di LINQ. Per altre informazioni, vedere queryLanguage-Integrated (LINQ).
Che cos'è una query parallela?
Una query PLINQ in molti modi è simile a una query LINQ to Objects non parallela. Le query PLINQ, esattamente come le query LINQ sequenziali, operano su qualsiasi origine dati in memoria IEnumerable o IEnumerable<T> e hanno un'esecuzione posticipata, il che significa che non iniziano l'esecuzione fino a quando la query non viene enumerata. La differenza principale è che PLINQ tenta di usare completamente tutti i processori nel sistema. Questa operazione viene eseguita partizionando l'origine dati in segmenti e quindi eseguendo la query su ogni segmento in thread di lavoro separati in parallelo su più processori. In molti casi, l'esecuzione parallela significa che la query viene eseguita in modo significativamente più veloce.
Tramite l'esecuzione parallela, PLINQ può ottenere miglioramenti significativi delle prestazioni rispetto al codice legacy per determinati tipi di query, spesso aggiungendo semplicemente l'operazione AsParallel di query all'origine dati. Tuttavia, il parallelismo può introdurre le proprie complessità e non tutte le operazioni di query vengono eseguite più velocemente in PLINQ. In effetti, la parallelizzazione rallenta effettivamente determinate query. È quindi necessario comprendere in che modo i problemi, ad esempio l'ordinamento, influiscono sulle query parallele. Per ulteriori dettagli, consulta Comprendere l'accelerazione in PLINQ.
Annotazioni
Questa documentazione usa espressioni lambda 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.
Nella parte restante di questo articolo viene fornita una panoramica delle classi PLINQ principali e viene illustrato come creare query PLINQ. Ogni sezione contiene collegamenti a informazioni e esempi di codice più dettagliati.
Classe ParallelEnumerable
La System.Linq.ParallelEnumerable classe espone quasi tutte le funzionalità di PLINQ. Il resto dei tipi dello spazio dei nomi System.Linq è compilato nell'assembly System.Core.dll. I progetti C# e Visual Basic predefiniti in Visual Studio fanno entrambi riferimento all'assembly e importano lo spazio dei nomi.
ParallelEnumerable include 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 (C#) e Introduzione a LINQ (Visual Basic).
Oltre agli operatori di query standard, la ParallelEnumerable classe contiene un set di metodi che consentono comportamenti specifici per l'esecuzione parallela. Questi metodi specifici di PLINQ sono elencati nella tabella seguente.
Operatore ParallelEnumerable | Descrizione |
---|---|
AsParallel | Punto di ingresso per PLINQ. Specifica che il resto della query deve essere parallelizzato, se possibile. |
AsSequential | Specifica che il resto della query deve essere eseguito in sequenza, come query LINQ non parallela. |
AsOrdered | Specifica che PLINQ deve mantenere l'ordinamento della sequenza di origine per il resto della query o fino a quando l'ordinamento non viene modificato, ad esempio utilizzando una clausola orderby (Order By in Visual Basic). |
AsUnordered | Specifica che PLINQ per il resto della query non è necessario per mantenere l'ordinamento della sequenza di origine. |
WithCancellation | Specifica che PLINQ deve monitorare periodicamente lo stato del token di annullamento fornito e annullare l'esecuzione se richiesto. |
WithDegreeOfParallelism | Specifica il numero massimo di processori che PLINQ deve usare per parallelizzare la query. |
WithMergeOptions | Fornisce un'indicazione su come PLINQ dovrebbe, se possibile, unire i risultati ottenuti in parallelo in una sola sequenza nel thread consumatore. |
WithExecutionMode | Specifica se PLINQ deve parallelizzare la query anche quando il comportamento predefinito deve essere eseguito in sequenza. |
ForAll | Un metodo di enumerazione multithreading che, a differenza dell'iterazione sui risultati della query, consente di elaborare i risultati in parallelo senza prima eseguire il merge al thread consumer. |
Aggregate sovraccarico | Un overload specifico per PLINQ che consente l'aggregazione intermedia tramite le partizioni locali del thread, insieme a una funzione di aggregazione finale per combinare i risultati di tutte le partizioni. |
Modello di consenso esplicito
Quando si scrive una query, scegliere di optare per PLINQ invocando 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($"{evenNums.Count()} even numbers out of {source.Count()} total");
// 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 lega gli operatori di query successivi, in questo caso where
e select
, alle implementazioni 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 query restituisca velocità in base alla parallelizzazione, PLINQ partiziona la sequenza di origine in attività che possono essere eseguite simultaneamente. Se non è sicuro parallelizzare una query, PLINQ esegue la query in sequenza. Se PLINQ ha una scelta tra un algoritmo parallelo potenzialmente costoso o un algoritmo sequenziale economico, sceglie l'algoritmo sequenziale per impostazione predefinita. È possibile usare il WithExecutionMode metodo e l'enumerazione System.Linq.ParallelExecutionMode per indicare a PLINQ di selezionare l'algoritmo parallelo. Ciò è utile quando si conosce testando e misurando che una determinata query viene eseguita più velocemente 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. È possibile indicare a PLINQ di usare non più di un numero specificato di processori usando il WithDegreeOfParallelism metodo . Ciò è utile quando si vuole assicurarsi che altri processi in esecuzione nel computer ricevano una certa quantità di tempo di CPU. Il frammento di codice seguente limita la query all'utilizzo di un massimo di 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 esegue una quantità significativa di lavoro non associato a calcolo, ad esempio I/O di file, può essere utile specificare un grado di parallelismo maggiore del numero di core nel computer.
Query parallele ordinate e non ordinate
In alcune query, un operatore di query deve produrre risultati che mantengono l'ordinamento della sequenza di origine. PLINQ fornisce l'operatore AsOrdered a questo scopo. AsOrdered è distinto da AsSequential. Una AsOrdered sequenza viene ancora elaborata in parallelo, ma i risultati vengono memorizzati nel buffer e ordinati. Poiché la conservazione degli ordini comporta in genere un lavoro aggiuntivo, una AsOrdered sequenza potrebbe essere elaborata più lentamente rispetto alla sequenza predefinita AsUnordered . L'eventuale velocità di un'operazione parallela ordinata specifica rispetto a una versione sequenziale dell'operazione dipende da molti fattori.
Nell'esempio di codice seguente viene illustrato come acconsentire esplicitamente alla 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 degli ordini in PLINQ.
Query Parallele vs. Sequenziali
Alcune operazioni richiedono che i dati di origine vengano recapitati in modo sequenziale. Gli ParallelEnumerable operatori di query ripristinano automaticamente la modalità sequenziale quando è necessario. Per gli operatori di query definiti dall'utente e i delegati utente che richiedono l'esecuzione sequenziale, PLINQ fornisce il AsSequential metodo . Quando si usa AsSequential, tutti gli operatori successivi nella query vengono eseguiti in sequenza fino a quando AsParallel non viene chiamato di nuovo. Per altre informazioni, vedere Guida: Combinare query LINQ parallele e sequenziali.
Opzioni per l'unione dei risultati della query
Quando una query PLINQ viene eseguita in parallelo, i risultati di ogni thread di lavoro devono essere uniti nuovamente al thread principale per l'utilizzo da parte di un foreach
ciclo (For Each
in Visual Basic) o l'inserimento in un elenco o in una matrice. In alcuni casi, potrebbe essere utile specificare un particolare tipo di operazione di merge, ad esempio per iniziare a produrre risultati più rapidamente. A questo scopo, PLINQ supporta il WithMergeOptions metodo e l'enumerazione ParallelMergeOptions . Per altre informazioni, vedere Opzioni di merge in PLINQ.
Operatore ForAll
Nelle query LINQ sequenziali, l'esecuzione viene posticipata fino a quando la query non viene enumerata in un foreach
ciclo (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 scorrere i risultati. Tuttavia, foreach
non viene eseguito in parallelo e pertanto richiede che l'output di tutte le attività parallele venga unito nuovamente al thread in cui è in esecuzione il ciclo. In PLINQ è possibile usare foreach
quando è necessario mantenere l'ordinamento finale dei risultati della query e anche ogni volta che si elaborano i risultati in modo seriale, ad esempio quando si chiama Console.WriteLine
per ogni elemento. Per velocizzare l'esecuzione delle query quando non è necessaria l'archiviazione degli ordini e quando l'elaborazione dei risultati può essere parallelizzata, usare il ForAll metodo per eseguire una query PLINQ.
ForAll non esegue questo passaggio di unione finale. Nell'esempio di codice seguente viene illustrato come usare il ForAll metodo .
System.Collections.Concurrent.ConcurrentBag<T> viene usato qui perché è ottimizzato per più thread che aggiungono simultaneamente senza tentare di rimuovere 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 illustra la differenza tra foreach
e ForAll per quanto riguarda l'esecuzione di query.
Annullamento
PLINQ è integrato con i tipi di annullamento in .NET. Per altre informazioni, vedere Annullamento nei thread gestiti. A differenza delle query LINQ to Objects sequenziali, pertanto, le query PLINQ possono essere annullate. Per creare una query PLINQ annullabile, usare l'operatore WithCancellation nella query e fornire un'istanza CancellationToken come argomento. Quando la IsCancellationRequested proprietà nel token è impostata su true, PLINQ lo noterà, 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 una maggiore velocità di risposta, è anche possibile rispondere alle richieste di annullamento in delegati utente con esecuzione prolungata. Per altre informazioni, vedere Procedura: Annullare una query PLINQ.
Eccezioni
Quando viene eseguita una query PLINQ, è possibile che vengano generate più eccezioni da thread diversi contemporaneamente. Inoltre, il codice per gestire l'eccezione potrebbe trovarsi in un thread diverso rispetto al codice che ha generato l'eccezione. PLINQ utilizza il tipo AggregateException per incapsulare tutte le eccezioni generate da una query e trasferire queste eccezioni al thread chiamante. Nel thread chiamante è necessario un solo blocco try-catch. Tuttavia, è possibile scorrere tutte le eccezioni incapsulate in AggregateException e gestire quelle da cui è possibile recuperare in modo sicuro. In rari casi, è possibile che vengano generate alcune eccezioni che non sono incapsulate in un AggregateException, e anche ThreadAbortException non sono incapsulate.
Quando le eccezioni possono risalire al thread di unione, è possibile che una query continui a elaborare alcuni elementi dopo che l'eccezione è stata sollevata.
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 una 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 (anche se i dati possono essere riassegnati dinamicamente a tali partizioni durante il runtime 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 partizionatori personalizzati per PLINQ e TPL.
Misurazione delle prestazioni PLINQ
In molti casi, una query può essere parallelizzata, ma l'overhead di configurazione della query parallela supera il vantaggio delle prestazioni ottenuto. Se una query non esegue molto calcolo o se l'origine dati è di piccole dimensioni, una query PLINQ potrebbe essere più lenta rispetto a una query LINQ to Objects sequenziale. È possibile usare Parallel Performance Analyzer in Visual Studio Team Server per confrontare le prestazioni di varie interrogazioni, individuare i bottleneck di elaborazione e determinare se l'interrogazione è in esecuzione in parallelo o in modo sequenziale. Per altre informazioni, vedere Visualizzatore di concorrenza e Procedura: Misurare le prestazioni delle query PLINQ.