Multithreading in C#, Teil 5: Parallele Schleifen mit Parallel.For und Parallel.ForEach entwickeln

Mittels Parallel.For und Parallel.ForEach lassen sich for und foreach Schleifen ausführen. Dabei werden die einzelnen Iterationsschritte aber parallel statt sequentiell verarbeitet. Dies kann bei zeitaufwendigen Schleifen zu einer Verbesserung der Anwendungsperformance führen.

 
Die Verwendung der parallelen Schleifen gestaltet sich im ersten Moment recht simpel. Es sollte dabei aber nicht ausser Acht gelassen werden, dass auch in diesem Fall die Besonderheiten der Multithreading Programmierung beachtet werden müssen. Dieser Artikel soll Ihnen daher das nötige Grundlagenwissen für den Einsatz der parallelen Schleifen vermitteln. Um den Rahmen des Artikels nicht zu sprengen werden die komplexeren Themengebiete wie zum Beispiel Error Handling in parallelen Schleifen im nächsten Artikel dieser Serien folgen.

 
Parallel.For

Die einfache Syntax einer parallelen for Schleife lautet: Parallel.For(Startwert, Endwert, Aktion)

Der Startwert ist dabei immer inklusive und der Endwert ist exklusive. Eine Schleife von Startwert 1 bis Endwert 10 führt somit zu Schleifendurchläufen für die Werte 1 bis 9. Das nachfolgende Beispiel zeigt eine einfache parallele Schleife. In der Konsole werden dabei die Werte 1 bis 9 ausgegeben. Da die Ausführung der Iterationsschritte parallel erfolgt ist die Reihenfolge der Ausgabewerte zufällig.

    1 static void Main(string[] args)

    2 {

    3     Parallel.For(1, 10, i => DoWork(i));

    4 

    5     Console.ReadKey();

    6 }

    7 

    8 static void DoWork(int i)

    9 {

   10     Console.Write(i + " ");

   11 }

 
Parallel.ForEach

Die Syntax einer parallelen foreach Schleife lautet: Parallel.ForEach(Enumeration, Aktion)

Das nachfolgende Beispiel zeigt eine Anwendung welche die Werte 1 bis 9 in der Konsole ausgibt. Wie im vorhergehenden Beispiel ist auch hier die Reihenfolge der ausgegebenen Werte zufällig.

    1 static void Main(string[] args)

    2 {

    3     List<int> values = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

    4 

    5     Parallel.ForEach(values, i => DoWork(i));

    6 

    7     Console.ReadKey();

    8 }

    9 

   10 static void DoWork(int i)

   11 {

   12     Console.Write(i + " ");

   13 }

 
Schleifen vorzeitig beenden mit Break

Sequentielle Schleifen lassen sich mittels des Schlüsselwortes break vorzeitig beenden. Bei den parallelen Versionen funktioniert dies nicht, da die angegebene Aktionsfunktion aufgerufen wird und somit kein direkter Schleifencode existiert in welchem das break Schlüsselwort eingesetzt werden könnte. Für parallele Schleifen steht daher ein anderer Mechanismus zu Verfügung. Die Klasse ParallelLoopState verwaltet den Status der Schleife und kann deren Ausführung unterbrechen. Der Aktionsfunktion lässt sich dazu der aktuelle Schleifenstatus übergeben.

Ein typischer Anwendungsfall könnte lauten: Führe die Abarbeitung der Schleife durch bis ein bestimmtes Abbruchkriterium erfüllt ist. In der ParallelLoopState Klasse steht dafür die Break() Funktion bereit. Ein Aufruf dieser Funktion führt dazu, dass keine weiteren Schleifendurchläufe ausgeführt werden. Des Weiteren kann in den bereits parallel laufenden Aktionsfunktionen geprüft werden ob das Abbruchkriterium gesetzt wurde. Dies kann mittels des Wertes ShouldExitCurrentIteration abgefragt werden.

Das nachfolgende Beispiel zeigt einen solchen Schleifenabbruch. Die Schleife wird abgebrochen wenn der Wert 10 erreicht ist.

    1 static void Main(string[] args)

    2 {

    3     List<int> values = new List<int>()

    4     {

    5         0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12

    6     };

    7 

    8     ParallelLoopResult result;

    9 

   10     result = Parallel.ForEach(values, (i, loopState) => DoWork(i, loopState));

   11 

   12     Console.WriteLine("result: " +

   13         result.IsCompleted + ", " +

   14         result.LowestBreakIteration);

   15 

   16     Console.ReadKey();

   17 }

   18 

   19 static void DoWork(int i, ParallelLoopState loopState)

   20 {

   21     if (i == 10)

   22     {

   23         loopState.Break();

   24     }

   25     else

   26     {

   27         if (loopState.ShouldExitCurrentIteration == false)

   28         {

   29             Console.Write(i + " ");

   30         }

   31     }

   32 }

 
Bei der Ausführung dieser Beispielanwendung führt der Aufruf von Break dazu, dasd alle Iterationen bis zu diesem Punkt bearbeitet werden. In dem Beispiel werden somit immer die Werte von 0 bis 9 in zufälliger Reihenfolge ausgegeben.

 
Parallele Schleifen geben als Rückgabewert ein ParallelLoopResult Objekt zurück. Dieses enthält die Werte IsCompleted und LowestBreakIteration. Bei einem Abbruch mittels Break Funktion ist IsCompleted immer false und LowestBreakIteration enthält die Nummer der ersten Iteration die den Abbruch ausgelöst hat.

Im obigen Beispiel ist IsCompleted somit false und LowestBreakIteration ist 10.

 
Schleifen vorzeitig beenden mit Stop

Im vorhergehenden Anwendungsfall sollten alle Elemente bis zum Erreichen des Abbruchkriteriums bearbeitet werden. Ein anderer typischer Anwendungsfall ist die Suche nach einem bestimmten Element. In solch einem Fall ist es nicht relevant ob die anderen Elemente der Schleife bearbeitet werden oder nicht. Ein sofortiger Abbruch einer parallelen Schleife kann mittels der Stop() Funktion der ParallelLoopState Klasse ausgelöst werden. In den bereits parallel laufenden Aktionsfunktionen kann mittels IsStopped geprüft werden, ob ein Abbruch forciert wurde.

Das nachfolgende Beispiel zeigt eine parallele Schleife welche sofort abgebrochen werden soll wenn der Wert 10 gefunden wurde.

    1 static void Main(string[] args)

    2 {

    3     List<int> values = new List<int>()

    4     {

    5         0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12

    6     };

    7 

    8     ParallelLoopResult result;

    9 

   10     result = Parallel.ForEach(values, (i, loopState) => DoWork(i, loopState));

   11 

   12     Console.WriteLine("result: " +

   13         result.IsCompleted + ", " +

   14         result.LowestBreakIteration);

   15 

   16     Console.ReadKey();

   17 }

   18 

   19 static void DoWork(int i, ParallelLoopState loopState)

   20 {

   21     if (i == 10)

   22     {

   23         loopState.Stop();

   24     }

   25     else

   26     {

   27         if (loopState.IsStopped == false)

   28         {

   29             Console.Write(i + " ");

   30         }

   31     }

   32 }

 
Dieser Code ähnelt sehr stark dem des vorhergehenden Beispiels. Bei Verwendung der Break Funktion werden alle Elemente bis zum Erreichen des Abbruchkriteriums prozessiert. Im Gegensatz dazu wird bei Einsatz der Stop Funktion sofort abgebrochen. Dies kann dazu führen das nur eine Teilmenge der Elemente bis zum Abbruchkriterium prozessiert wird. In der Beispielanwendung werden somit die Werte 0 bis 9 oder eine Teilmenge dieser Werte in beliebiger Reihenfolge ausgegeben.

 
Wie zuvor ist der Rückgabewert ein ParallelLoopResult Objekt. Auch in diesem Fall hat das Element IsCompleted den Wert false. Im Gegensatz zur vorhergehenden Funktion hat das Element LowestBreakIteration aber den Wert null.

 
Verschachtelte Schleifen

Werden Schleifen verschachtelt dann sollten parallele Schleifen für die äusseren Schleifen verwendet werden. Dies vermindert den Verwaltungsaufwand Threads da weniger Threads mit grösseren Aufgaben erzeugt werden anstatt sehr vieler Threads mit kleinen Aufgaben.

De nachfolgende Quellcode zeigt ein solches Beispiel. Die äussere Schleife wird dabei parallel und die innere Schleife sequentiell ausgeführt.

    1 static void Main(string[] args)

    2 {

    3     Parallel.For(1, 5, i =>

    4         {

    5             for (int k = 1; k < 10; k++)

    6             {

    7                 DoWork(i, k);

    8             }

    9         });

   10 

   11     Console.ReadKey();

   12 }

   13 

   14 static void DoWork(int i, int k)

   15 {

   16     Console.Write(i + "," + k + " ");

   17 }

 
Fazit

Mittels Parallel.For und Parallel.ForEach lassen sich Schleifen programmieren deren Iterationen parallel ausgeführt werden. Die Threadverwaltung wird dabei komplett vom Framework übernommen. Einfache parallele Schleifen, deren Iterationsschritte unabhängig voneinander erfolgen, lassen sich dadurch auf einfache Weise erstellen. Im nächsten Artikel dieser Artikelserie werde ich Ihnen zeigen wie die Iterationsschritte gemeinsame Aufgaben erledigen können und wie die Fehlerbehandlung in parallelen Schleifen erfolgen kann.

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