Freigeben über


Einführung in PLINQ

Parallel LINQ (PLINQ) ist eine parallele Implementierung des Language-Integrated Query (LINQ) -Musters. PLINQ implementiert den vollständigen Satz von LINQ-Standardabfrageoperatoren als Erweiterungsmethoden für den System.Linq Namespace und verfügt über zusätzliche Operatoren für parallele Vorgänge. PLINQ kombiniert die Einfachheit und Lesbarkeit der LINQ-Syntax mit der Leistungsfähigkeit der parallelen Programmierung.

Tipp

Wenn Sie mit LINQ nicht vertraut sind, verfügt es über ein einheitliches Modell zum Abfragen einer aufzählbaren Datenquelle auf typsichere Weise. LINQ to Objects ist der Name für LINQ-Abfragen, die für In-Memory-Auflistungen wie List<T> z. B. Arrays ausgeführt werden. In diesem Artikel wird davon ausgegangen, dass Sie ein grundlegendes Verständnis von LINQ haben. Weitere Informationen finden Sie unter Language-Integrated Query (LINQ).For more information, seeLanguage-Integrated Query (LINQ).

Was ist eine Parallel-Abfrage?

Eine PLINQ-Abfrage ähnelt in vielerlei Hinsicht einer nicht parallelen LINQ to Objects-Abfrage. PLINQ-Abfragen, genau wie sequenzielle LINQ-Abfragen, funktionieren in einem beliebigen Speicher IEnumerable oder IEnumerable<T> einer Datenquelle und haben verzögerte Ausführung, d. h., sie beginnen erst, wenn die Abfrage aufgezählt wird. Der Hauptunterschied besteht darin, dass PLINQ versucht, alle Prozessoren auf dem System vollständig zu nutzen. Dazu partitionieren Sie die Datenquelle in Segmente, und führen Sie dann die Abfrage für jedes Segment in separaten Arbeitsthreads parallel auf mehreren Prozessoren aus. In vielen Fällen bedeutet die parallele Ausführung, dass die Abfrage deutlich schneller ausgeführt wird.

Durch parallele Ausführung kann PLINQ erhebliche Leistungsverbesserungen gegenüber Legacycode für bestimmte Arten von Abfragen erzielen, häufig nur durch Hinzufügen des AsParallel Abfragevorgangs zur Datenquelle. Parallelität kann jedoch eigene Komplexitäten einführen, und nicht alle Abfragevorgänge werden in PLINQ schneller ausgeführt. Tatsächlich verlangsamt die Parallelisierung tatsächlich bestimmte Abfragen. Daher sollten Sie verstehen, wie sich Probleme wie die Sortierung auf parallele Abfragen auswirken. Weitere Informationen finden Sie unter Understanding Speedup in PLINQ.

Hinweis

In dieser Dokumentation werden Lambda-Ausdrücke verwendet, um Stellvertretungen in PLINQ zu definieren. Wenn Sie mit Lambda-Ausdrücken in C# oder Visual Basic nicht vertraut sind, lesen Sie Lambda-Ausdrücke in PLINQ und TPL.

Im restlichen Teil dieses Artikels finden Sie eine Übersicht über die wichtigsten PLINQ-Klassen und erläutert, wie PLINQ-Abfragen erstellt werden. Jeder Abschnitt enthält Links zu ausführlicheren Informationen und Codebeispielen.

Die ParallelEnumerable-Klasse

Die System.Linq.ParallelEnumerable Klasse macht fast alle Funktionen von PLINQ verfügbar. Sie und die restlichen System.Linq Namespacetypen werden in der System.Core.dll Assembly kompiliert. Die standardmäßigen C#- und Visual Basic-Projekte in Visual Studio verweisen auf die Assembly und importieren den Namespace.

ParallelEnumerable enthält Implementierungen aller Standardabfrageoperatoren, die LINQ to Objects unterstützen, obwohl nicht versucht wird, die einzelnen Zusätze zu parallelisieren. Wenn Sie mit LINQ nicht vertraut sind, lesen Sie "Einführung in LINQ (C#) und Einführung in LINQ (Visual Basic)".

Zusätzlich zu den Standardabfrageoperatoren enthält die ParallelEnumerable Klasse eine Reihe von Methoden, die verhaltensweisen, die für die parallele Ausführung spezifisch sind. Diese PLINQ-spezifischen Methoden sind in der folgenden Tabelle aufgeführt.

ParallelEnumerable-Operator BESCHREIBUNG
AsParallel Der Einstiegspunkt für PLINQ. Gibt an, dass der Rest der Abfrage parallelisiert werden soll, falls möglich.
AsSequential Gibt an, dass der Rest der Abfrage sequenziell als nicht parallele LINQ-Abfrage ausgeführt werden soll.
AsOrdered Gibt an, dass PLINQ die Reihenfolge der Quellsequenz für den Rest der Abfrage beibehalten soll, oder bis die Sortierung geändert wird, z. B. durch die Verwendung einer Orderby -Klausel (Order By in Visual Basic).
AsUnordered Gibt an, dass PLINQ für den Rest der Abfrage nicht erforderlich ist, um die Reihenfolge der Quellsequenz beizubehalten.
WithCancellation Gibt an, dass PLINQ regelmäßig den Status des bereitgestellten Abbruchtokens überwachen und die Ausführung abbrechen soll, wenn sie angefordert wird.
WithDegreeOfParallelism Gibt die maximale Anzahl von Prozessoren an, die PLINQ zum Parallelisieren der Abfrage verwenden soll.
WithMergeOptions Enthält einen Hinweis darauf, wie PLINQ, falls möglich, parallele Ergebnisse wieder in nur einer Sequenz im verbrauchten Thread zusammenführen soll.
WithExecutionMode Gibt an, ob PLINQ die Abfrage parallelisieren soll, auch wenn das Standardverhalten die Abfrage sequenziell ausführen würde.
ForAll Eine Multithread-Enumerationsmethode, mit der im Gegensatz zum Durchlaufen der Ergebnisse der Abfrage Ergebnisse parallel verarbeitet werden können, ohne zuerst mit dem Consumerthread zusammenzuführen.
Aggregate überlasten Eine für PLINQ eindeutige Überladung und ermöglicht die Zwischenaggregation über threadlokale Partitionen sowie eine endgültige Aggregationsfunktion, um die Ergebnisse aller Partitionen zu kombinieren.

Das Opt-In-Modell

Wenn Sie eine Abfrage schreiben, melden Sie sich bei PLINQ an, indem Sie die ParallelEnumerable.AsParallel Erweiterungsmethode für die Datenquelle aufrufen, wie im folgenden Beispiel gezeigt.

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

Die AsParallel Erweiterungsmethode bindet die nachfolgenden Abfrageoperatoren in diesem Fall where und selectan die System.Linq.ParallelEnumerable Implementierungen.

Ausführungsmodi

PLINQ ist standardmäßig konservativer. Zur Laufzeit analysiert die PLINQ-Infrastruktur die Gesamtstruktur der Abfrage. Wenn die Abfrage wahrscheinlich Geschwindigkeiten durch Parallelisierung liefert, partitioniert PLINQ die Quellsequenz in Aufgaben, die gleichzeitig ausgeführt werden können. Wenn es nicht sicher ist, eine Abfrage zu parallelisieren, führt PLINQ einfach die Abfrage sequenziell aus. Wenn PLINQ eine Wahl zwischen einem potenziell kostspieligen Parallelalgorithmus oder einem kostengünstigen sequenziellen Algorithmus hat, wählt er standardmäßig den sequenziellen Algorithmus aus. Sie können die WithExecutionMode Methode und die System.Linq.ParallelExecutionMode Enumeration verwenden, um PLINQ anzuweisen, den parallelen Algorithmus auszuwählen. Dies ist nützlich, wenn Sie wissen, dass eine bestimmte Abfrage parallel ausgeführt wird. Weitere Informationen finden Sie unter How to: Specify the Execution Mode in PLINQ.

Grad der Parallelität

PLINQ verwendet standardmäßig alle Prozessoren auf dem Hostcomputer. Sie können PLINQ anweisen, nicht mehr als eine bestimmte Anzahl von Prozessoren mithilfe der WithDegreeOfParallelism Methode zu verwenden. Dies ist nützlich, wenn Sie sicherstellen möchten, dass andere auf dem Computer ausgeführte Prozesse eine bestimmte CPU-Zeit erhalten. Der folgende Codeausschnitt beschränkt die Abfrage auf die Verwendung von maximal zwei Prozessoren.

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

In Fällen, in denen eine Abfrage eine erhebliche Menge nicht rechengebundener Arbeit wie Datei-E/A durchführt, kann es von Vorteil sein, einen Grad an Parallelität anzugeben, der größer als die Anzahl der Kerne auf dem Computer ist.

Sortierte und nicht sortierte parallele Abfragen

In einigen Abfragen muss ein Abfrageoperator Ergebnisse erzeugen, die die Reihenfolge der Quellsequenz beibehalten. PLINQ stellt den AsOrdered Operator zu diesem Zweck bereit. AsOrdered unterscheidet sich von AsSequential. Eine AsOrdered Sequenz wird weiterhin parallel verarbeitet, aber die Ergebnisse werden gepuffert und sortiert. Da die Erhaltung von Bestellungen in der Regel zusätzliche Arbeit erfordert, kann eine AsOrdered Sequenz langsamer verarbeitet werden als die Standardsequenz AsUnordered . Ob ein geordneter paralleler Vorgang schneller ist als eine sequenzielle Version des Vorgangs, hängt von vielen Faktoren ab.

Im folgenden Codebeispiel wird gezeigt, wie Sie sich für die Erhaltung der Reihenfolge entscheiden.

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


Weitere Informationen finden Sie unter "Order Preservation" in PLINQ.

Parallele und sequenzielle Abfragen

Einige Vorgänge erfordern, dass die Quelldaten sequenziell übermittelt werden. Die ParallelEnumerable Abfrageoperatoren werden bei Bedarf automatisch in den sequenziellen Modus zurückgesetzt. Für benutzerdefinierte Abfrageoperatoren und Benutzerdelegatten, die eine sequenzielle Ausführung erfordern, stellt PLINQ die AsSequential Methode bereit. Bei Verwendung AsSequentialwerden alle nachfolgenden Operatoren in der Abfrage sequenziell ausgeführt, bis AsParallel sie erneut aufgerufen wird. Weitere Informationen finden Sie unter How to: Combine Parallel and Sequential LINQ Queries.

Optionen für das Zusammenführen von Abfrageergebnissen

Wenn eine PLINQ-Abfrage parallel ausgeführt wird, müssen die Ergebnisse jedes Arbeitsthreads wieder mit dem Hauptthread zusammengeführt werden, um eine foreach Schleife (in Visual Basic) zu verwenden oderFor Each in eine Liste oder ein Array einzufügen. In einigen Fällen kann es von Vorteil sein, eine bestimmte Art von Zusammenführungsvorgang anzugeben, z. B. um schneller mit der Herstellung von Ergebnissen zu beginnen. Zu diesem Zweck unterstützt PLINQ die WithMergeOptions Methode und die ParallelMergeOptions Enumeration. Weitere Informationen finden Sie unter "Zusammenführungsoptionen" in PLINQ.

Der ForAll-Operator

In sequenziellen LINQ-Abfragen wird die Ausführung zurückgestellt, bis die Abfrage in einer foreach (For Each in Visual Basic)-Schleife oder durch Aufrufen einer Methode wie , oder durch Aufrufen einer Methode wie ToList , ToArray oder ToDictionary. In PLINQ können Sie die Abfrage auch foreach ausführen und die Ergebnisse durchlaufen. foreach Selbst wird jedoch nicht parallel ausgeführt und erfordert daher, dass die Ausgabe aller parallelen Aufgaben wieder in den Thread zusammengeführt wird, auf dem die Schleife ausgeführt wird. In PLINQ können Sie verwenden foreach , wenn Sie die endgültige Reihenfolge der Abfrageergebnisse beibehalten müssen, und auch, wenn Sie die Ergebnisse auf serielle Weise verarbeiten, z. B. wenn Sie für jedes Element aufrufen Console.WriteLine . Verwenden Sie ForAll die Methode zum Ausführen einer PLINQ-Abfrage, um eine PLINQ-Abfrage auszuführen, um eine Order-Erhaltung nicht erforderlich zu machen, und wenn die Verarbeitung der Ergebnisse selbst parallelisiert werden kann. ForAll führt diesen letzten Zusammenführungsschritt nicht aus. Das folgende Codebeispiel zeigt, wie die ForAll Methode verwendet wird. System.Collections.Concurrent.ConcurrentBag<T> wird hier verwendet, da sie für mehrere Threads optimiert ist, die gleichzeitig hinzugefügt werden, ohne zu versuchen, Elemente zu entfernen.

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

Die folgende Abbildung zeigt den Unterschied zwischen foreach und ForAll hinsichtlich der Abfrageausführung.

ForAll vs. ForEach

Abbruch

PLINQ ist in die Abbruchtypen in .NET integriert. (Weitere Informationen finden Sie unter "Abbruch" in verwalteten Threads.) Daher können PLINQ-Abfragen im Gegensatz zu sequenziellen LINQ to Objects-Abfragen abgebrochen werden. Verwenden Sie zum Erstellen einer abbrechbaren PLINQ-Abfrage den WithCancellation Operator für die Abfrage, und stellen Sie eine CancellationToken Instanz als Argument bereit. Wenn die IsCancellationRequested Eigenschaft für das Token auf "true" festgelegt ist, bemerkt PLINQ sie, beendet die Verarbeitung für alle Threads und löst ein OperationCanceledException.

Es ist möglich, dass eine PLINQ-Abfrage einige Elemente weiter verarbeitet, nachdem das Abbruchtoken festgelegt wurde.

Um eine höhere Reaktionsfähigkeit zu erreichen, können Sie auch auf Abbruchanforderungen in lang ausgeführten Benutzerstellvertretungen reagieren. Weitere Informationen finden Sie unter How to: Cancel a PLINQ Query.

Ausnahmen

Wenn eine PLINQ-Abfrage ausgeführt wird, werden möglicherweise mehrere Ausnahmen gleichzeitig von verschiedenen Threads ausgelöst. Außerdem kann sich der Code zum Behandeln der Ausnahme auf einem anderen Thread als der Code, der die Ausnahme ausgelöst hat, auf einem anderen Thread als der Code, der die Ausnahme ausgelöst hat. PLINQ verwendet den AggregateException Typ, um alle Ausnahmen zu kapseln, die von einer Abfrage ausgelöst wurden, und diese Ausnahmen zurück an den aufrufenden Thread zu marshallen. Im aufrufenden Thread ist nur ein Try-Catch-Block erforderlich. Sie können jedoch alle Ausnahmen durchlaufen, die in der AggregateException Kapselung sind und alle, von denen Sie sich sicher wiederherstellen können, abfangen. In seltenen Fällen können einige Ausnahmen ausgelöst werden, die nicht in einen AggregateExceptionUmbruch eingeschlossen sind, und ThreadAbortExceptions werden ebenfalls nicht umschlossen.

Wenn Ausnahmen wieder in den Verknüpfungsthread eingeblasen werden dürfen, ist es möglich, dass eine Abfrage einige Elemente weiter verarbeitet, nachdem die Ausnahme ausgelöst wurde.

Weitere Informationen finden Sie unter How to: Handle Exceptions in a PLINQ Query.

Benutzerdefinierte Partitionierer

In einigen Fällen können Sie die Abfrageleistung verbessern, indem Sie einen benutzerdefinierten Partitionierer schreiben, der einige Merkmale der Quelldaten nutzt. In der Abfrage ist der benutzerdefinierte Partitionierer selbst das aufzählbare Objekt, das abgefragt wird.

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 unterstützt eine feste Anzahl von Partitionen (obwohl Daten während der Laufzeit für den Lastenausgleich dynamisch neu zugewiesen werden können).) For und ForEach nur dynamische Partitionierung unterstützen, was bedeutet, dass sich die Anzahl der Partitionen zur Laufzeit ändert. Weitere Informationen finden Sie unter Benutzerdefinierte Partitionierer für PLINQ und TPL.

Messen der PLINQ-Leistung

In vielen Fällen kann eine Abfrage parallelisiert werden, aber der Aufwand beim Einrichten der parallelen Abfrage überwiegt den Leistungsvorteil. Wenn eine Abfrage nicht viel Berechnung durchführt oder die Datenquelle klein ist, ist eine PLINQ-Abfrage möglicherweise langsamer als eine sequenzielle LINQ to Objects-Abfrage. Sie können die Parallel Performance Analyzer in Visual Studio Team Server verwenden, um die Leistung verschiedener Abfragen zu vergleichen, um Verarbeitungsengpässe zu finden und festzustellen, ob die Abfrage parallel oder sequenziell ausgeführt wird. Weitere Informationen finden Sie unter Concurrency Visualizer und How to: Measure PLINQ Query Performance.

Siehe auch