Einführung in PLINQ

Parallel LINQ (PLINQ) ist eine parallele Implementierung des LINQ-Musters (Language-Integrated Query). PLINQ implementiert den kompletten 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

LINQ bietet ein einheitliches Modell für die typsichere Abfrage beliebiger aufzählbarer Datenquellen. LINQ to Objects ist der Name für LINQ-Abfragen von Auflistungen im Arbeitsspeicher, z. B. List<T>, und Arrays. Dieser Artikel setzt Grundkenntnisse in LINQ voraus. Weitere Informationen finden Sie unter Language-Integrated Query (LINQ).

Was ist eine parallele Abfrage?

Eine PLINQ-Abfrage entspricht weitgehend einer nicht parallelen LINQ to Objects-Abfrage. PLINQ-Abfragen werden, ebenso wie sequenzielle LINQ-Abfragen, auf alle IEnumerable- oder IEnumerable<T>-Datenquellen im Arbeitsspeicher angewendet und weisen eine verzögerte Ausführung auf, d. h. sie werden erst ausgeführt, wenn die Abfrage aufgelistet wird. Der Hauptunterschied besteht darin, dass PLINQ versucht, alle Prozessoren im System vollständig auszuschöpfen. PLINQ partitioniert hierzu die Datenquelle in Segmente und führt dann die Abfrage für jedes Segment parallel in separaten Arbeitsthreads und auf mehreren Prozessoren aus. In vielen Fällen bedeutet eine parallele Ausführung, dass die Abfrage deutlich schneller ausgeführt wird.

Durch die parallele Ausführung kann PLINQ für bestimmte Abfragen eine erheblich höhere Leistung als Legacycode erzielen. Häufig muss hierzu lediglich der AsParallel-Abfragevorgang zur Datenquelle hinzugefügt werden. Die Parallelität kann jedoch eigene Komplexitäten mit sich bringen, und nicht alle Abfragevorgänge werden in PLINQ schneller ausgeführt. Im Gegenteil, manche Abfragen werden durch die Parallelisierung sogar verlangsamt. Sie sollten daher wissen, wie sich bestimmte Aspekte, z. B. Sortierung, auf parallele Abfragen auswirken. Weitere Informationen finden Sie unter Understanding Speedup in PLINQ (Grundlagen zur Beschleunigung in PLINQ).

Hinweis

In dieser Dokumentation werden Delegaten in PLINQ mithilfe von Lambdaausdrücken definiert. Falls Sie mit der Verwendung von Lambda-Ausdrücken in C# oder Visual Basic nicht vertraut sind, finden Sie entsprechende Informationen unter Lambda Expressions in PLINQ and TPL (Lambda-Ausdrücke in PLINQ und TPL).

Der Rest dieses Artikels bietet 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 stellt nahezu die gesamte Funktionalität von PLINQ zur Verfügung. Diese Klasse sowie die restlichen Typen des System.Linq-Namespace werden in der Assembly "System.Core.dll" kompiliert. Die standardmäßigen C#- und Visual Basic-Projekte in Visual Studio verweisen auf die Assembly und importiert den Namespace.

ParallelEnumerable enthält Implementierungen aller Standardabfrageoperatoren, die von LINQ to Objects unterstützt werden, auch wenn nicht für jeden eine Parallelisierung angestrebt wird. Wenn Sie nicht mit LINQ 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 nach Möglichkeit parallelisiert werden soll.
AsSequential Gibt an, dass der Rest der Abfrage sequenziell, als nicht parallele LINQ-Abfrage ausgeführt werden soll.
AsOrdered Gibt an, diese PLINQ die Reihenfolge der Quellsequenz im Rest der Abfrage oder bis zu einer Änderung der Reihenfolge, z. B. durch eine orderby-Klausel (Ordner By in Vlsual Basic), beibehalten soll.
AsUnordered Gibt an, dass PLINQ die Reihenfolge der Quellsequenz im Rest der Abfrage nicht beibehalten muss.
WithCancellation Gibt an, diese PLINQ den Zustand des bereitgestellten Abbruchtokens in regelmäßigen Abständen überwachen und die Ausführung auf Anforderung abbrechen soll.
WithDegreeOfParallelism Gibt die maximale Anzahl von Prozessoren an, die PLINQ zum Parallelisieren der Abfrage verwenden soll.
WithMergeOptions Gibt an, wie PLINQ parallele Ergebnisse im Consumerthread wieder in einer Sequenz zusammenführen soll, sofern dies möglich ist.
WithExecutionMode Gibt an, ob PLINQ die Abfrage parallelisieren soll, selbst wenn diese standardmäßig sequenziell ausgeführt 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-Überladung 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

Beim Schreiben einer Abfrage entscheiden Sie sich für PLINQ, indem Sie in der Datenquelle die ParallelEnumerable.AsParallel-Erweiterungsmethode 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("{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

Die AsParallel-Erweiterungsmethode bindet die nachfolgenden Abfrageoperatoren, in diesem Fall, where und select, an die System.Linq.ParallelEnumerable-Implementierungen.

Ausführungsmodi

Standardmäßig wird PLINQ konservativ ausgeführt. Die PLINQ-Infrastruktur analysiert zur Laufzeit die Gesamtstruktur der Abfrage. Wenn die Abfrage voraussichtlich durch eine Parallelisierung beschleunigt werden kann, partitioniert PLINQ die Quellsequenz in Aufgaben, die gleichzeitig ausgeführt werden können. Kann eine Abfrage nicht sicher parallelisiert werden, führt PLINQ die Abfrage sequenziell aus. Wenn PLINQ die Wahl zwischen einem potenziell rechenintensiven parallelen Algorithmus und einem einfachen sequenziellen Algorithmus hat, entscheidet es sich standardmäßig für den sequenziellen Algorithmus. Mit der WithExecutionMode-Methode und der System.Linq.ParallelExecutionMode-Enumeration können Sie PLINQ anweisen, den parallelen Algorithmus auszuwählen. Dies ist sinnvoll, wenn Sie aufgrund von Tests und Messungen wissen, dass eine bestimmte Abfrage im parallelen Modus schneller ausgeführt wird. Weitere Informationen finden Sie unter Vorgehensweise: Angeben des Ausführungsmodus in PLINQ.

Grad der Parallelität

Standardmäßig verwendet PLINQ alle Prozessoren des Hostcomputers. Mit der WithDegreeOfParallelism-Methode können Sie PLINQ anweisen, nicht mehr als eine angegebene Anzahl von Prozessoren zu verwenden. Sie können so sicherstellen, dass andere auf dem Computer ausgeführt Prozesse eine bestimmte Menge an CPU-Zeit erhalten. Der folgende Codeausschnitt beschränkt die Abfrage auf 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

Wenn eine Abfrage eine große Mengen an nicht rechnergebundenen Aufgaben ausführt, z. B. Datei-E/A, kann es von Vorteil sein, einen höheren Grad an Parallelität als die auf dem Rechner verfügbare Anzahl von Kernen anzugeben.

Geordnete und ungeordnete parallele Abfragen

Bei einigen Abfragen muss ein Abfrageoperator Ergebnisse erzeugen, die die Reihenfolge der Quellsequenz beibehalten. PLINQ stellt für diesen Zweck den AsOrdered-Operator bereit. AsOrdered unterscheidet sich von AsSequential. Eine AsOrdered-Sequenz wird weiterhin parallel ausgeführt, die Ergebnisse werden jedoch gepuffert und sortiert. Da die Beibehaltung der Reihenfolge in der Regel einen gewissen Mehraufwand bedeutet, wird eine AsOrdered-Sequenz ggf. langsamer verarbeitet als die standardmäßige AsUnordered-Sequenz. Ob ein bestimmter geordneter paralleler Vorgang schneller ist als eine sequenzielle Version des Vorgangs, hängt von vielen Faktoren ab.

Im folgenden Codebeispiel wird gezeigt, wie Sie die Beibehaltung der Reihenfolge aktivieren.

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 (Beibehaltung der Reihenfolge in PLINQ).

Parallele und sequenzielle Abfragen

Einige Vorgänge erfordern, dass die Quelldaten sequenziell übergeben werden. Die ParallelEnumerable-Abfrageoperatoren stellen automatisch den sequenziellen Modus wieder her, wenn dies erforderlich ist. Für benutzerdefinierte Abfrageoperatoren und Benutzerdelegaten, 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 Vorgehensweise: Kombinieren von parallelen und sequenziellen LINQ-Abfragen.

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 ist es hilfreich, eine bestimmte Art von Merge anzugeben, z. B. um Ergebnisse schneller zu erzeugen. Zu diesem Zweck unterstützt PLINQ die WithMergeOptions-Methode und die ParallelMergeOptions-Enumeration. Weitere Informationen finden Sie unter Merge Options in PLINQ (Zusammenführungsoptionen in PLINQ).

Der ForAll-Operator

In sequenziellen LINQ-Abfragen wird die Ausführung verzögert, bis die Abfrage entweder in einer foreach-Schleife (For Each in Visual Basic) oder durch das Aufrufen einer Methode, z. B. 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. foreach selbst wird jedoch nicht parallel ausgeführt, sodass die Ausgabe aller parallelen Aufgaben wieder in dem Thread, in dem die Schleife ausgeführt wird, zusammengeführt werden muss. In PLINQ können Sie foreach verwenden, wenn Sie die abschließende Reihenfolge der Abfrageergebnisse beibehalten möchten oder wenn Sie die Ergebnisse seriell verarbeiten, z. B. wenn Sie für jedes Element Console.WriteLine aufrufen. Wenn die Beibehaltung der Reihenfolge nicht erforderlich ist und die Verarbeitung der Ergebnisse selbst parallelisiert werden kann, verwenden Sie die ForAll-Methode, um die Ausführung der PLINQ-Abfrage zu beschleunigen. ForAll führt die abschließende Zusammenführung nicht aus. Im folgenden Codebeispiel wird die Verwendung der ForAll-Methode veranschaulicht. System.Collections.Concurrent.ConcurrentBag<T> wird hier verwendet, da sie für mehrere Threads optimiert ist, die gleichzeitig Elemente hinzufügen, ohne dass versucht wird, 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 mit den Abbruchtypen in .NET integriert. (Weitere Informationen finden Sie unter Cancellation in Managed Threads (Abbruch in verwalteten Threads).) Daher können PLINQ-Abfragen im Gegensatz zu sequenziellen LINQ to Objects-Abfragen abgebrochen werden. Um eine abbrechbare PLINQ-Abfrage zu erstellen, verwenden Sie den WithCancellation-Operator in der Abfrage, und stellen Sie eine CancellationToken-Instanz als Argument bereit. Wenn die IsCancellationRequested-Eigenschaft im Token auf „true“ festgelegt ist, erkennt PLINQ dies. Die Verarbeitung wird in diesem Fall in allen Threads abgebrochen, und eine OperationCanceledException wird ausgelöst.

Es ist möglich, dass eine PLINQ-Abfrage nach dem Festlegen des Abbruchtokens weiterhin einige Elemente verarbeitet.

Um kürzere Reaktionszeiten zu erzielen, können Sie auch auf Abbruchanforderungen in Benutzerdelegaten mit langer Laufzeit reagieren. Weitere Informationen finden Sie unter Vorgehensweise: Abbrechen einer PLINQ-Abfrage.

Ausnahmen

Bei der Ausführung einer PLINQ-Abfrage können mehrere Ausnahmen von verschiedenen Threads gleichzeitig ausgelöst werden. Zudem kann sich der Code für die Behandlung einer Ausnahme in einem anderen Thread befinden als der Code, der die Ausnahme ausgelöst hat. PLINQ kapselt alle Ausnahmen, die von einer Abfrage ausgelöst wurden, mithilfe des AggregateException-Typs und marshallt diese Ausnahmen zurück an den aufrufenden Thread. 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 mittels Bubbling wieder an den Verbindungsthread übergeben werden können, ist es möglich, dass eine Abfrage nach dem Auslösen der Ausnahme weiterhin einige Elemente verarbeitet.

Weitere Informationen finden Sie unter Vorgehensweise: Behandeln von Ausnahmen in einer PLINQ-Abfrage.

Benutzerdefinierte Partitionierer

Sie können die Abfrageleistung teilweise erhöhen, indem Sie einen benutzerdefinierten Partitionierer schreiben, der bestimmte Merkmale der Quelldaten nutzt. In der Abfrage ist der benutzerdefinierte Partitionierer das auflistbare 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 (auch wenn die Daten während der Laufzeit diesen Partitionen dynamisch neu zugeordnet werden können, um einen Lastenausgleich zu gewährleisten). For und ForEach unterstützen nur die dynamische Partitionierung, d. h. die Anzahl der Partitionen ändert sich zur Laufzeit. Weitere Informationen finden Sie unter Custom Partitioners for PLINQ and TPL (Benutzerdefinierte Partitionierer für PLINQ und TPL).

Messen der PLINQ-Leistung

In vielen Fällen kann eine Abfrage parallelisiert werden, der mit dem Einrichten der parallelen Abfrage verbundene Mehraufwand überwiegt jedoch die erzielte Leistungssteigerung. Wenn eine Abfrage nur wenige Berechnung ausführt oder wenn die Datenquelle klein ist, ist eine PLINQ-Abfrage möglicherweise langsamer als ein sequenzielle LINQ to Objects-Abfrage. Mithilfe des Parallel Performance Analyzer in Visual Studio Team Server können Sie die Leistung verschiedener Abfragen vergleichen, Verarbeitungsengpässe suchen und bestimmen, ob die Abfrage parallel oder sequenziell ausgeführt wird. Weitere Informationen finden Sie unter Parallelitätsschnellansicht und Vorgehensweise: Messen der Leistung von PLINQ-Abfragen.

Siehe auch