Multithreading in C#, Teil 18: PLINQ (Parallel LINQ)

In einem vorhergehenden Artikel habe ich Ihnen LINQ vorgestellt. Heute möchte ich daran anknüpfen und Ihnen zeigen wie Sie LINQ Anweisungen auf mehre Prozessoren aufteilen und damit parallel ausführen können. Gerade bei aufwendigen Anweisungen, welche eine hohe Ausführungszeit aufweisen, kann durch die parallele Berechnung eine spürbar höhere Ausführungsgeschwindigkeit erreicht werden. Die Spracherweiterung LINQ bietet hierfür sehr einfach zu verwendende und leistungsstarke Zusatzfunktionen. Diese Zusatzfunktionen sind unter dem Begriff PLINQ oder auch Parallel LINQ bekannt.

 
Select mittels PLINQ

Wie einfach PLINQ anzuwenden ist, möchte ich Ihnen anhand eines Beispiels zeigen. Betrachten wir zum Vergleich erst einmal eine normale sequentielle LINQ Anweisung, welche auf nur einem Prozessor ausgeführt wird. Der nachfolgende Quellcode zeigt eine Anweisung, mit welcher alle geraden Zahlen aus der Liste ermittelt werden. Die Resultate werden anschliessend in der Konsole ausgegeben.

List<int> values;

values = new List<int>(){1,2,4,6,8,12,15,17,18,22,27};

var results = from value in values
              where value % 2 == 0
              select value;

foreach (var result in results)
{
    Console.Write(result + ",");
}

 
Die Anwendung erzeugt folgende Ausgabe.

2,4,6,8,12,18,22,

 
Diese einfache Abfrage möchte ich im nächsten Schritt auf PLINQ umstellen, damit eine parallele Verteilung des Rechenaufwands auf alle Prozessoren meines PCs erfolgt. PLINQ bietet für diesen Zweck Erweiterungsmethoden die sich nahtlos in bestehende LINQ Abfragen integrieren lassen. Das vorhergehende Beispiel kann daher mittels einer weiteren kleinen Funktion auf PLINQ umgestellt werden. Der nachfolgende Quellcode zeigt diese neue Funktion.

List<int> values;

values = new List<int>() { 1, 2, 4, 6, 8, 12, 15, 17, 18, 22, 27 };

var results = from value in values.AsParallel()
              where value % 2 == 0
              select value;

foreach (var result in results)
{
    Console.Write(result + ",");
}

 
In Zeile 5 wurde die Datenquelle mit der Funktion AsParallel erweitert. Diese Funktion bewirkt die Aufteilung der Datenquelle in mehrere Pakete. Die Abarbeitung der Datenpakete erfolgt anschliessend auf mehreren Threads verteilt und damit parallel. Am Ende der Ausführung werden die einzelnen Teilergebnisse wiederrum zu einem Gesamtergebnis zusammengeführt.

 
Die Anwendung erzeugt nun folgende Ausgabe.

22,2,6,18,4,8,12,

 
Wie Sie sehen können ist diese Datenmenge nicht mehr sortiert. Dies verdeutlicht die Aufteilung, parallele Berechnung und Kombination der Teilergebnisse bei der Ausführung der PLINQ Anweisung. Die Daten wurden scheinbar in vier Pakete aufgeteilt (mein PC hat vier Kerne) parallel berechnet und die Ergebnisse danach zusammengefasst. Da die parallelen Berechnungen unterschiedlich schnell erfolgen ist die Ergebnismenge nicht mehr sortiert. Die Reihenfolge ist vielmehr als zufällig anzusehen und kann sich bei jeder Ausführung der Anweisung ändern.

Wenn es für Ihren Anwendungsfall zwingend nötig ist, dass die original Elementreihenfolge beibehalten wird, so können Sie dies erzwingen. Dazu kann nach der AsParallel Anweisung einfach eine AsOrdered Anweisung angehängt werden: myData.AsParallel().AsOrdered(). Die Elemente in der Ergebnismenge sind dann zwar wieder in entsprechenden Reihenfolge wie bei einer sequentiellen LINQ Anweisung, aber Sie erkaufen sich diese Funktionalität mit einer langsameren Ausführung der Anweisung da PLINQ intern eine entsprechend aufwändige Datenverwaltung durchführen muss.

 
Einschränkungen von PLINQ

Wie bereits beschrieben wird bei Verwendung der Funktion AsParallel die Datenmenge aufgeteilt, parallel verarbeitet und die Teilergebnisse werden zu einem Gesamtergebnis vereint. Ist dieser Verwaltungsaufwand aber grösser als der Zeitgewinn der parallelen Ausführung, dann kann eine PLINQ Anweisung langsamer als eine entsprechende LINQ Anweisung sein. PLINQ erkennt solche Fälle weitestgehend und führt dann automatisch eine sequentielle Abarbeitung der Anweisung durch.

Einige Funktionen eignen sich weniger gut für eine Parallelisierung. Dazu gehören beispielsweise die Funktionen Join, Union, Distinct und GroupBy. Diese Funktionen können zwar parallelisiert ausgeführt werden, dabei entsteht aber ein sehr hoher Verwaltungsaufwand. Dies führt des Öfteren dazu, dass die sequentielle Funktionsausführung schneller als die parallele Funktionsausführung ist. PLINQ kann diese Fälle nicht mit Sicherheit vorhersagen. Daher sollten Sie bei diesen Funktionen selbst und jeweils anwendungsfallspezifisch prüfen ob sich der Einsatz von PLINQ lohnt.

Innerhalb von LINQ Anweisungen können Sie eigene Funktionen und Befehle nutzen. Dies funktioniert entsprechend auch bei PLINQ. Hierbei müssen Sie aber zwangsläufig selbst sicherstellen, dass die verwendeten Funktionen threadsicher implementiert sind. PLINQ teilt die Ausführung der Anweisung auf mehrere Threads auf. Daher werden auch die externen Zusatzfunktionen aus mehreren Threads heraus aufgerufen und müssen daher threadsicher sein.

 
Fazit

PLINQ ist sehr einfach anzuwenden. Auch die Umstellung bestehender LINQ Anweisungen erfolgt mit geringem Aufwand. Als Softwareentwickler sollten Sie sich aber nicht verleiten lassen zu locker mit PLINQ umzugehen. Sie begeben sich damit in die Untiefen der Multithreading-Entwicklung und müssen daher mit entsprechender Sorgfalt vorgehen.

Werbung
Dieser Beitrag wurde unter .NET, C#, Multithreading abgelegt und mit , verschlagwortet. Setze ein Lesezeichen auf den Permalink.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit deinem WordPress.com-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s