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> und 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ützt, obwohl kein Versuch unternommen wird, jeden einzelnen 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 einen Satz von Methoden, die spezielle Verhaltensweisen für die parallele Ausführung unterstützen. 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 Gibt einen Hinweis darauf, wie PLINQ, wenn möglich, parallele Ergebnisse zu einer einzigen Sequenz auf dem verbrauchenden 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 Multithreadenumerationsmethode, die, im Gegensatz zum Durchlaufen der Ergebnisse der Abfrage, eine parallele Verarbeitung der Ergebnisse ermöglicht, ohne dass diese zuvor im Consumerthread zusammengeführt werden.
Aggregate Überlast Eine spezielle Überladung für PLINQ, die eine Zwischenaggregation über lokale Threadpartitionen sowie eine abschließende Aggregationsfunktion zum Kombinieren der Ergebnisse aller Partitionen ermöglicht.

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. Falls die Abfrage voraussichtlich Geschwindigkeitssteigerungen durch Parallelisierung ermöglicht, partitioniert PLINQ die Datenquelle 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 durch Tests und Messungen herausgefunden haben, dass eine bestimmte Abfrage schneller 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 dafür entscheiden, die Reihenfolge beizubehalten.

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 von AsSequential werden alle nachfolgenden Operatoren in der Abfrage sequenziell ausgeführt, bis AsParallel 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 aller Arbeitsthreads wieder im Hauptthread zusammengeführt werden, damit sie in einer foreach-Schleife (For Each in Visual Basics) verwendet oder in eine Liste bzw. ein Array eingefügt werden können. 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-Schleife (in Visual Basic For Each) oder durch Aufrufen einer Methode wie ToList, ToArray oder ToDictionary aufgezählt wird. In PLINQ können Sie zudem foreach verwenden, um die Abfrage auszuführen und die Ergebnisse zu durchlaufen. Jedoch wird foreach selbst nicht parallel ausgeführt und erfordert daher, dass das Ergebnis aller parallelen Tasks 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 die Methode ForAll, um eine PLINQ-Abfrage auszuführen, wenn eine schnellere Abfrageausführung gewünscht wird, die Erhaltung der Reihenfolge nicht erforderlich ist und die Verarbeitung der Ergebnisse 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 und 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 Eigenschaft IsCancellationRequested des Tokens auf „true“ gesetzt ist, bemerkt PLINQ dies, stoppt die Verarbeitung auf allen Threads und löst eine OperationCanceledException aus.

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

Um kürzere Reaktionszeiten zu erzielen, können Sie auch auf Abbruchanforderungen in Benutzerdelegaten mit langer Laufzeit 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 befinden 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 AggregateException gekapselt sind, und die Ausnahmen erfassen, die Sie sicher beheben können. In seltenen Fällen können Ausnahmen ausgelöst werden, die nicht in einer AggregateException eingeschlossen sind. ThreadAbortExceptions sind ebenfalls nicht eingeschlossen.

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 dynamisch für den Lastenausgleich 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