Wprowadzenie do PLINQ

Parallel LINQ (PLINQ) jest równoległą implementacją wzorca języka CLR . PLINQ implementuje pełen zestaw standardowych operatorów zapytań LINQ jako metody rozszerzenia dla System.Linq przestrzeni nazw i ma dodatkowe operatory dla operacji równoległych. PLINQ łączy prostotę i czytelność składni LINQ z możliwością programowania równoległego.

Porada

Jeśli nie znasz programu LINQ, oferuje on jednolity model służący do wykonywania zapytań dotyczących dowolnego wyliczalnego źródła danych w sposób bezpieczny dla tego typu. LINQ to Objects to nazwa zapytań LINQ, które są uruchamiane względem kolekcji w pamięci, takich jak List<T> tablice. W tym artykule przyjęto założenie, że masz podstawową wiedzę na temat LINQ. Aby uzyskać więcej informacji, zobacz temat Language-Integrated Query (LINQ).

Co to jest zapytanie równoległe?

Zapytanie PLINQ na wiele sposobów przypomina nierównoległe zapytanie LINQ to Objects. PLINQ zapytania, podobnie jak zapytania sekwencyjne LINQ, działają na dowolnym z pamięci IEnumerable lub w IEnumerable<T> źródle danych i mają odroczone wykonanie, co oznacza, że nie są uruchamiane, dopóki zapytanie nie zostanie wyliczone. Podstawowa różnica polega na tym, że PLINQ próbuje w pełni korzystać ze wszystkich procesorów w systemie. Robi to przez partycjonowanie źródła danych do segmentów, a następnie wykonywanie zapytania dla każdego segmentu w oddzielnych wątkach roboczych równolegle na wielu procesorach. W wielu przypadkach wykonywanie równoległe oznacza, że zapytanie działa znacznie szybciej.

Za pośrednictwem wykonywania równoległego PLINQ może osiągnąć znaczną poprawę wydajności w przypadku niektórych rodzajów zapytań, często tylko przez dodanie AsParallel operacji zapytania do źródła danych. Równoległość może jednak wprowadzać własne złożoności, a nie wszystkie operacje zapytań są wykonywane szybciej w PLINQ. W rzeczywistości przetwarzanie równoległe w rzeczywistości spowalnia niektóre zapytania. W związku z tym należy zrozumieć, jak problemy, takie jak porządkowanie, wpływają na zapytania równoległe. Aby uzyskać więcej informacji, zobacz temat Omówienie przyspieszenie w PLINQ.

Uwaga

Ta dokumentacja używa wyrażeń lambda do definiowania delegatów w PLINQ. Jeśli nie znasz wyrażeń lambda w języku C# lub Visual Basic, zobacz lambda Expressions in PLINQ and TPL.

Pozostała część tego artykułu zawiera omówienie głównych klas PLINQ i omawia sposób tworzenia zapytań PLINQ. Każda sekcja zawiera linki do bardziej szczegółowych informacji i przykładów kodu.

Klasa ParallelEnumerable

System.Linq.ParallelEnumerableKlasa uwidacznia prawie wszystkie funkcje PLINQ. I reszta System.Linq typów przestrzeni nazw są kompilowane do zestawu System.Core.dll. Domyślne projekty C# i Visual Basic w programie Visual Studio odwołują się do zestawu i importują przestrzeń nazw.

ParallelEnumerable zawiera implementacje wszystkich standardowych operatorów zapytań, które są obsługiwane przez LINQ to Objects, chociaż nie podejmuje próby zrównoleglanie każdego z nich. Jeśli nie znasz programu LINQ, zobacz Introduction to LINQ (C#) i Introduction to LINQ (Visual Basic).

Oprócz standardowych operatorów zapytań ParallelEnumerable Klasa zawiera zestaw metod, które umożliwiają zachowanie specyficzne dla wykonywania równoległego. Te metody specyficzne dla PLINQ są wymienione w poniższej tabeli.

Operator ParallelEnumerable Opis
AsParallel Punkt wejścia dla PLINQ. Określa, że pozostała część zapytania powinna być równoległa, jeśli jest to możliwe.
AsSequential Określa, że pozostała część zapytania powinna być uruchamiana sekwencyjnie, jako nierównoległe zapytanie LINQ.
AsOrdered Określa, że PLINQ powinna zachować kolejność sekwencji źródłowej pozostałej części zapytania lub do czasu zmiany kolejności, na przykład za pomocą klauzuli OrderBy (order by w Visual Basic).
AsUnordered Określa, że PLINQ dla pozostałej części zapytania nie jest wymagana do zachowania kolejności sekwencji źródłowej.
WithCancellation Określa, że PLINQ powinien okresowo monitorować stan podanego tokenu anulowania i anulować wykonywanie, jeśli jest to wymagane.
WithDegreeOfParallelism Określa maksymalną liczbę procesorów, które PLINQ powinny być używane do zrównoleglanie zapytania.
WithMergeOptions Zawiera wskazówkę dotyczącą sposobu, w jaki PLINQ powinna, jeśli jest to możliwe, scalanie równoległych wyników z powrotem do jednej sekwencji w wątku zużywanym.
WithExecutionMode Określa, czy PLINQ ma zrównoleglanie zapytanie, nawet gdy domyślne zachowanie będzie uruchamiane sekwencyjnie.
ForAll Metoda wyliczania wielowątkowego, która, w przeciwieństwie do iteracji wyników zapytania, umożliwia równoległe przetwarzanie wyników bez wcześniejszego scalania z powrotem do wątku odbiorcy.
Aggregate występują Przeciążenie, które jest unikatowe dla PLINQ i włącza agregację pośrednią względem partycji lokalnych wątków, oraz ostateczną funkcję agregacji, aby połączyć wyniki wszystkich partycji.

Model zgody

Po zapisaniu zapytania należy zadecydować, aby PLINQ przez wywołanie ParallelEnumerable.AsParallel metody rozszerzenia źródła danych, jak pokazano w poniższym przykładzie.

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

AsParallelMetoda rozszerzenia wiąże kolejne operatory zapytań, w tym przypadku where i select , do System.Linq.ParallelEnumerable implementacji.

Tryby wykonywania

Domyślnie PLINQ jest ostrożność. W czasie wykonywania infrastruktura PLINQ analizuje ogólną strukturę zapytania. Jeśli zapytanie może dać przyspieszenia przez przetwarzanie równoległe, PLINQ podzieli sekwencję źródłową z zadaniami, które mogą być uruchamiane współbieżnie. Jeśli zrównoleglanie zapytania nie jest bezpieczne, PLINQ po prostu uruchamia kwerendę sekwencyjnie. Jeśli PLINQ ma wybór między potencjalnie kosztownym algorytmem równoległym lub niedrogim algorytmem sekwencyjnym, wybiera algorytm sekwencyjny domyślnie. Możesz użyć WithExecutionMode metody i System.Linq.ParallelExecutionMode wyliczenia, aby polecić PLINQ, aby wybrać algorytm równoległy. Jest to przydatne, gdy wiadomo, że testy i pomiary są wykonywane szybciej przez określone zapytanie. Aby uzyskać więcej informacji, zobacz How to: Określanie trybu wykonywania w PLINQ.

Stopień równoległości

Domyślnie PLINQ używa wszystkich procesorów na komputerze-hoście. Można wydać PLINQ do użycia nie więcej niż określoną liczbę procesorów przy użyciu WithDegreeOfParallelism metody. Jest to przydatne, jeśli chcesz upewnić się, że inne procesy uruchomione na komputerze otrzymują określoną ilość czasu procesora CPU. Poniższy fragment kodu ogranicza zapytanie do wykorzystania maksymalnie dwóch procesorów.

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

W przypadkach, gdy zapytanie wykonuje znaczną ilość pracy niezwiązanej z obliczeniami, taką jak pliki we/wy, warto określić stopień równoległości większy niż liczba rdzeni na komputerze.

Uporządkowane i nieuporządkowane zapytania równoległe

W niektórych zapytaniach operator zapytania musi generować wyniki, które zachowują kolejność sekwencji źródłowej. PLINQ zapewnia AsOrdered operator do tego celu. AsOrdered różni się od AsSequential . AsOrderedSekwencja jest nadal przetwarzana równolegle, ale jej wyniki są buforowane i sortowane. Ponieważ zachowanie kolejności zwykle wymaga dodatkowej pracy, AsOrdered sekwencja może być przetwarzana wolniej niż domyślna AsUnordered . Czy określona uporządkowana operacja równoległa jest szybsza niż sekwencyjna wersja operacji, zależy od wielu czynników.

Poniższy przykład kodu pokazuje, jak zrezygnować z zachowania porządku.

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


Aby uzyskać więcej informacji, zobacz temat porządkowania zamówień w PLINQ.

Zapytania równoległe a sekwencyjne

Niektóre operacje wymagają, aby dane źródłowe były dostarczane w sposób sekwencyjny. ParallelEnumerableOperatory zapytań powracają do trybu sekwencyjnego automatycznie, gdy jest to wymagane. W przypadku operatorów zapytań zdefiniowanych przez użytkownika i delegatów użytkowników, którzy wymagają wykonania sekwencyjnego, PLINQ udostępnia AsSequential metodę. Gdy używasz AsSequential , wszystkie kolejne operatory w zapytaniu są wykonywane sekwencyjnie do momentu, gdy AsParallel zostanie wywołane ponownie. Aby uzyskać więcej informacji, zobacz How to: łączenie równoległych i sekwencyjnych zapytań LINQ.

Opcje scalania wyników zapytania

Gdy zapytania PLINQ są wykonywane równolegle, jego wyniki z każdego wątku roboczego muszą zostać scalone z powrotem do głównego wątku w celu użycia przez foreach pętlę ( For Each w Visual Basic) lub wstawić do listy lub tablicy. W niektórych przypadkach może być korzystne określenie określonego rodzaju operacji scalania, na przykład w celu szybszego tworzenia wyników. W tym celu PLINQ obsługuje WithMergeOptions metodę i ParallelMergeOptions Wyliczenie. Aby uzyskać więcej informacji, zobacz Opcje scalania w PLINQ.

Operator ForAll

W sekwencyjnych zapytaniach LINQ wykonywanie jest odroczone do momentu wyliczenia zapytania w foreach For Each pętli (w Visual Basic) lub przez wywołanie metody takiej jak ToList , ToArray lub ToDictionary . W programie PLINQ można także użyć foreach do wykonania zapytania i iteracji przez wyniki. Jednak foreach samo nie działa równolegle i dlatego wymaga, aby dane wyjściowe ze wszystkich zadań równoległych były scalane z powrotem do wątku, w którym jest uruchomiona pętla. W programie PLINQ można użyć, foreach gdy należy zachować ostateczne porządkowanie wyników zapytania, a także za każdym razem, gdy przetwarzasz wyniki w sposób szeregowy, na przykład w przypadku wywoływania Console.WriteLine dla każdego elementu. Aby przyspieszyć wykonywanie zapytań, gdy zachowywanie kolejności nie jest wymagane i gdy przetwarzanie wyników może być równoległe, użyj metody, ForAll Aby wykonać zapytanie PLINQ. ForAll nie wykonuje tego końcowego kroku scalania. Poniższy przykład kodu pokazuje, jak używać ForAll metody. System.Collections.Concurrent.ConcurrentBag<T> jest używany w tym miejscu, ponieważ jest zoptymalizowany pod kątem wielu wątków, które dodają współbieżność bez próby usunięcia jakichkolwiek elementów.

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)))

Na poniższej ilustracji przedstawiono różnicę między foreach i ForAll w odniesieniu do wykonywania zapytań.

ForAll a ForEach

Anulowanie

PLINQ jest zintegrowany z typami anulowania w programie .NET. (Aby uzyskać więcej informacji, zobacz Anulowanie w zarządzanych wątkach.) W związku z tym, w przeciwieństwie do sekwencyjnych zapytań LINQ to Objects, można anulować zapytania PLINQ. Aby utworzyć zapytanie PLINQ z możliwością anulowania, użyj WithCancellation operatora w zapytaniu i podaj CancellationToken wystąpienie jako argument. Gdy IsCancellationRequested Właściwość w tokenie ma wartość true, PLINQ będzie zauważyć, Zatrzymaj przetwarzanie we wszystkich wątkach i zgłosić OperationCanceledException .

Istnieje możliwość, że zapytanie PLINQ może nadal przetwarzać niektóre elementy po ustawieniu tokenu anulowania.

Aby zwiększyć czas odpowiedzi, można także odpowiedzieć na żądania anulowania w długotrwałych delegatach użytkowników. Aby uzyskać więcej informacji, zobacz How to: Cancel a PLINQ Query.

Wyjątki

Gdy wykonywane jest zapytanie PLINQ, wiele wyjątków może być generowanych z różnych wątków jednocześnie. Ponadto kod obsługujący wyjątek może znajdować się w innym wątku niż kod, który wywołał wyjątek. PLINQ używa AggregateException typu do hermetyzacji wszystkich wyjątków zgłoszonych przez zapytanie i przekierować te wyjątki z powrotem do wątku wywołującego. W wątku wywołującym wymagany jest tylko jeden blok try-catch. Można jednak wykonać iterację we wszystkich wyjątkach, które są hermetyzowane w AggregateException i przechwytywać dowolne z nich, z których można bezpiecznie odzyskać. W rzadkich przypadkach mogą zostać zgłoszone pewne wyjątki, które nie są opakowane w AggregateException , a ThreadAbortException s nie są również opakowane.

Gdy wyjątki mogą być rzutowane z powrotem do wątku sprzęgania, istnieje możliwość, że zapytanie może nadal przetwarzać niektóre elementy po wystąpieniu wyjątku.

Aby uzyskać więcej informacji, zobacz How to: obsługa wyjątków w zapytaniu PLINQ.

Niestandardowe partycje

W niektórych przypadkach można poprawić wydajność zapytań, pisząc niestandardowy program do partycjonowania, który wykorzystuje pewną charakterystykę danych źródłowych. W zapytaniu jest to obiekt, który jest wyliczalny.

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 obsługuje stałą liczbę partycji (mimo że dane mogą być dynamicznie przypisywane do tych partycji w czasie wykonywania równoważenia obciążenia). For i ForEach obsługują tylko partycjonowanie dynamiczne, co oznacza, że liczba partycji zmienia się w czasie wykonywania. Aby uzyskać więcej informacji, zobacz niestandardowe partycje dla PLINQ i TPL.

Mierzenie wydajności PLINQ

W wielu przypadkach zapytanie może być równoległe, ale obciążenie związane z konfigurowaniem zapytania równoległego przewyższa uzyskane korzyści z wydajności. Jeśli zapytanie nie wykonuje dużo obliczeń lub źródło danych jest małe, zapytanie PLINQ może być wolniejsze niż sekwencyjne zapytanie LINQ to Objects. Można użyć analizatora wydajności równoległej w programie Visual Studio Team Server do porównania wydajności różnych zapytań, lokalizowania wąskich gardeł przetwarzania oraz określania, czy zapytanie działa równolegle, czy sekwencyjnie. Aby uzyskać więcej informacji, zobacz temat Concurrency Visualizer i instrukcje: mierzenie wydajności zapytań PLINQ.

Zobacz także