Home > Task Parallel Library > TPL Teil 4 – Die Klasse Parallel

TPL Teil 4 – Die Klasse Parallel

Neben der Klasse Task, die im zweiten Teil vorgestellt wurde, bietet die Klasse Parallel mit die interessantesten Neuerungen in der Task Parallel Library (TPL). Sowohl das Parallel.For-Konstrukt als auch das Parallel.ForEach-Konstrukt stellt eine konkurrenzlos einfache Möglichkeit bereit, um aufwändige Berechnungen zu parallelisieren und damit deutlich zu beschleunigen.

Trotzdem muss ich den Hinweis aussprechen: Auch mit der TPL muss der Entwickler einige Dinge im Auge behalten. Doch nimmt die TPL einem sehr viele Verwaltungsaufgaben ab, die bisher der Entwickler selber bewältigen musste. Bei der Programmierung mit der Klasse Thread kann der Quellcode schnell sehr unübersichtlich werden und ist somit nicht immer einfach zu pflegen.

Die Klasse Parallel

Die statische Klasse Parallel enthält drei Methoden, die natürlich auch alle statisch sind:

  • Invoke()
  • For()
  • ForEach()

Jede Methode liegt in zahlreichen Überladungen vor.

Parallel.Invoke()

Sollen Codebereiche unabhängig voneinander ausgeführt werden, so bietet sich die Methode Parallel.Invoke() an. Die statische Methode liegt in zwei Überladungen vor:

public static void Invoke(params Action[] actions);
public static void Invoke(ParallelOptions parallelOptions, params Action[] actions);

Die einfachste Variante enthält ein Parameter-Array des Delegates Action(). Es können also keine Parameter übergeben oder zurückgeliefert werden.

Console.WriteLine("Start Run");
Parallel.Invoke(() => { Console.WriteLine("Start Task 1");
                        for (int i = 0; i < 10; i++)
                            Thread.Sleep(200);
                        Console.WriteLine("End Task 1"); },
                () => { Console.WriteLine("Start Task 2");
                        for (int i = 0; i < 20; i++)
                            Thread.Sleep(200);
                        Console.WriteLine("End Task 2"); },
                () => { Console.WriteLine("Start Task 3");
                        for (int i = 0; i < 30; i++)
                            Thread.Sleep(200);
                        Console.WriteLine("End Task 3"); });
Console.WriteLine("End Run");
Console.ReadLine();

Beispiel 1 (Visual Studio 2012)

Bis zu zehn Codebereiche können auf diese Weise parallel ausgeführt werden. Die Methode ist synchron, was bedeutet, es geht nach der Methode Parallel.Invoke() erst dann weiter, wenn alle Codebereiche vollständig ausgeführt wurden. Man sollte sich auch nicht darauf verlassen, dass der erste Action-Delegate immer vor dem zweiten ausgeführt wird.

Picture01

Bei der zweiten Variante kann als erster Parameter eine Variable vom Typ ParallelOptions übergeben werden. Zwei Eigenschaften dieser Klasse sind besonders von Interesse. Zum einem ist es die Eigenschaft MaxDegreeOfParallelism und zum anderen die Eigenschaft CancellationToken. Über diese kann das Laufzeitverhalten der TPL beeinflusst werden.

ClassParallelOptions

Grad der Parallelität (MaxDegreeOfParallelism)

Manchmal kann es notwendig sein, den Grad der Parallelisierung zu ändern, also die maximale Anzahl der parallelen Ausführungseinheiten festzulegen. Ein Wert von 1 entspricht hierbei einer sequenziellen Ausführung.

Console.WriteLine("Start Run");
Parallel.Invoke(new ParallelOptions() { MaxDegreeOfParallelism = 1 },
() => { Console.WriteLine("Start Task 1");
        for (int i = 0; i < 10; i++)
            Thread.Sleep(200);
        Console.WriteLine("End Task 1"); },
() => { Console.WriteLine("Start Task 2");
        for (int i = 0; i < 20; i++)
            Thread.Sleep(200);
        Console.WriteLine("End Task 2"); },
() => { Console.WriteLine("Start Task 3");
        for (int i = 0; i < 30; i++)
            Thread.Sleep(200);
        Console.WriteLine("End Task 3"); });
Console.WriteLine("End Run");
Console.ReadLine();

Es ist sehr gut zu erkennen, dass die Methode Invoke() die Action-Delegates sequenziell ausführt.

Picture02

Ich nutze gerne diese Möglichkeit, um die Skalierbarkeit einer Anwendung zu testen. Ein gut skalierbarer Algorithmus sollte bei einer Reduzierung von MaxDegreeOfParallelism auch entsprechend langsamer ausgeführt werden. Ist keine Veränderung am Laufzeitverhalten festzustellen, so ist der Algorithmus zu überarbeiten. Ist dieses nicht möglich, so ist die Verwendung der TPL in Frage zu stellen.

Mehr zum Thema Beurteilung der Performanceänderungen am Ende des Posts.

vorzeitiger Abbruch (CancellationToken)

Die TPL stellt mit dem Cancellation-Framework einen einheitlichen Mechanismus zur Verfügung, um Tasks kooperativ abbrechen zu können. Gelegentlich wird für diese Aufgabe auch einfach eine globale Variable verwendet. Die einzelnen Tasks fragen zyklisch diese Variable ab und beenden sich selbständig, sobald dieses gesetzt wurde. Ein solches Vorgehen wäre auch mit der TPL möglich, stellt aber einen höheren Aufwand dar. So sorgt das Cancellation-Framework dafür, dass der Rückgabewert des Task-Objektes richtig gesetzt wird. Schließlich soll die Unterscheidung zwischen Abbruch und Beenden eines Tasks auch nach außen weitergereicht werden.

Auch die Methode Parallel.Invoke() unterstützt durch die Klasse ParallelOptions dieses Framework. Vorgestellt wurde dieses schon unter TPL Teil 2 – Die Klasse Task. Deshalb starte ich direkt mit einem Beispiel:

public void Run()
{
    Console.WriteLine("Start Run");
    CancellationTokenSource cts = new CancellationTokenSource();
    ParallelOptions po = new ParallelOptions() { CancellationToken = cts.Token };
    Task t = Task.Run(() => Work(po), cts.Token);  // new in .NET 4.5
    Console.WriteLine("Cancel in 3sec");
    cts.CancelAfter(3000);  // new in .NET 4.5
    Console.WriteLine("End Run");
    Console.ReadLine();
}
private void Work(ParallelOptions po)
{
    Console.WriteLine("Start Work");
    Parallel.Invoke(po,
    () =>
    {
        Console.WriteLine("Start Task 1");
        for (int i = 0; i < 10; i++)
        {
            if (po.CancellationToken.IsCancellationRequested)
            {
                Console.WriteLine("Cancel Task 1");
                break;
            }
            Thread.Sleep(200);
        }
        Console.WriteLine("End Task 1");
    },
    () =>
    {
        Console.WriteLine("Start Task 2");
        for (int i = 0; i < 20; i++)
        {
            if (po.CancellationToken.IsCancellationRequested)
            {
                Console.WriteLine("Cancel Task 2");
                break;
            }
            Thread.Sleep(200);
        }
        Console.WriteLine("End Task 2");
    },
    () =>
    {
        Console.WriteLine("Start Task 3");
        for (int i = 0; i < 30; i++)
        {
            if (po.CancellationToken.IsCancellationRequested)
            {
                Console.WriteLine("Cancel Task 3");
                break;
            }
            Thread.Sleep(200);
        }
        Console.WriteLine("End Task 3");
    });
    Console.WriteLine("End Work");
}

Beispiel 2 (Visual Studio 2012)

Nach ca. 3 Sekunden wird der Abbruch aktiviert. Innerhalb dieser Zeit kann Task 1 seine Arbeit abschließen, da dieser nur 2 Sekunden benötigt. Task 2 und Task 3 hingegen werden vorzeitig abgebrochen.

Picture03

Der Abbruch erfolgt vollständig kooperativ, mit dem Vorteil, dass jede Ausführungseinheit selbst den Zeitpunkt des Abbruchs bestimmt und somit Ressourcen kontrolliert wieder freigeben kann.

Neu in .NET 4.5

Mit .NET 4.5 kamen einige neue Methoden hinzu. So kann jetzt mit der statischen Methode Task.Run() ein Task direkt erzeugt und eingeplant werden. Bisher war hierzu immer noch die Eigenschaft Factory notwendig.

Task t = Task.Run(() => Work(po), cts.Token); // new in .NET 4.5

Task t = Task.Factory.StartNew(() => Work(po), cts.Token); // also possible

Ebenfalls neu ist in der Klasse CancellationTokenSource die asynchrone Methode CancelAfter(). Als Parameter wird eine Zeitspanne angegeben, ab der der Abbruch ausgelöst wird.

cts.CancelAfter(3000);  // new in .NET 4.5
Einschränkungen

Folgende Einschränkungen sollten bei der Benutzung von Parallel.Invoke() beachtet werden:

Die einzelnen Ausführungseinheiten dürfen nicht zeitgleich auf dieselben Objekte oder Variablen schreibend zugreifen.

Aus den aufgerufenen Methoden darf nicht direkt auf das GUI zugegriffen werden. Auch hier gilt: nur der Thread, der die GUI-Elemente erzeugt, darf auf diese zugreifen.

An den Ausführungseinheiten können keine Parameter übergeben werden, da der parameterlose Delegate Action() benutzt wird. Auch sind Rückgabewerte nicht möglich.

Parallel.For()

Die Methode Parallel.For() bietet eine sehr einfache Möglichkeit, Schleifendurchläufe zu parallelisieren. Diese ist mehrfach überladen, wobei der grundsätzliche Aufbau dem einer üblichen for-Anweisung ähnelt:

public static ParallelLoopResult For(int fromInclusive,
                                     int toExclusive,
                                     Action<int> body);

Der Startindex fromInclusive wird über den ersten und der Endindex toExclusive über den zweiten Parameter übergeben. Hierbei ist der Startindex der erste Wert mit dem die Schleife aufgerufen wird und der Endindex minus 1 der letzte Wert.

Startindex, Endindex und Action-Delegate sind vom Datentyp int. Von jeder Variante der Methode Parallel.For() gibt es noch eine Überladung, die als Datentyp long verwendet.

Die Methode wird synchron zum Aufrufer ausgeführt. Erst wenn die Schleife komplett abgearbeitet wurde, wird mit dem folgenden Befehl fortgefahren. Das ist auch sinnvoll, da es bei dieser Methode nur darum geht, eine Schleife parallel auszuführen (also zu beschleunigen). Weiteres manuelles Synchronisieren ist dadurch überflüssig.

Der Code, der ausgeführt werden soll, wird per Delegate an den dritten Parameter übergeben. Der Delegate Action<int> erwartet in diesem Fall nur einen Parameter vom Typ int und hat keinen Rückgabewert. Der Delegate wird bei jeder Iteration mit dem aktuellen Index aufgerufen.

double[] arr = new double[100000];
Parallel.For(0, 100000, i =>
     {
        arr[i] = Math.Sin(i) + Math.Sqrt(i) * Math.Pow(i, 3.1415);
     });

Doch wie wird der Code parallel ausgeführt? Schlicht und einfach wird versucht, die anstehende Aufgabe gleichmäßig auf die CPU-Kerne zu verteilen. So könnte die Schleife auf einem Quad-Core in vier separate Schleifen aufgeteilt werden und jeder CPU-Kern wird mit der Abarbeitung beauftragt. Der erste CPU-Kern bearbeitet die Schleifen von 0 bis 24.999, der zweite CPU-Kern von 25.000 bis 49.999, der dritte von 50.000 bis 74.999 und der vierte von 75.000 bis 99.999. Die notwendigen Berechnungen und das Aufteilen auf die CPU-Kerne übernimmt das .NET-Framework. Man darf jetzt allerdings nicht erwarten, dass unter allen Umständen die Schleife viermal schneller abgearbeitet wird.

Im TPL Teil 1 – Einführung habe ich einige Performanz-Messungen durchgeführt. Dort ist gut zu erkennen, dass eine Steigerung nur dann effektiv erreicht wird, wenn die Anzahl der Schleifendurchläufe möglichst groß ist. Es muss also ausreichend ‘Arbeit’ vorhanden sein, die auf die einzelnen CPU-Kerne verteilt werden kann.

ParallelOption

Der Methode kann ebenfalls eine Variable vom Typ ParallelOptions übergeben werden. Die Bedeutung ist die gleiche wie bei der Methode Invoke() (siehe oben).

double[] arr = new double[100000];
ParallelOptions op = new ParallelOptions() { MaxDegreeOfParallelism = 4 };
Parallel.For(0, 100000, op, i =>
        {
            arr[i] = Math.Sin(i) + Math.Sqrt(i) * Math.Pow(i, 3.1415);
        });

Ein Beispiel zur Benutzung des Cancellation-Framworks folgt weiter unten.

Schrittweite des Iterators

Wie kann die Schrittweite des Iterators beeinflusst werden? Dieses ist leider nicht möglich. Der Index ist immer vom Typ long oder int und besitzt immer die Schrittweite +1. Der 2. Parameter (toExlusive) muss immer größer sein, als der erste (fromInclusive). Sind andere Schrittweiten notwendig, so muss eine Umrechnung des Iterators im Schleifencode erfolgen. Das folgende Beispiel führt die Schleife nur mit geraden Werte von 0 bis 98 aus (0, 2, 4, 6, … 98).

Parallel.For(0, 50, (i) =>
    {
        int myIndex = i * 2;
        arr[myIndex] = Math.Sin(myIndex);
    });
Rückgabewert mit ParallelLoopResult

Die Methode gibt die Struktur ParallelLoopResult zurück. Diese enthält zwei Eigenschaften (IsCompleted und LowestBreakIteration), die darüber Auskunft geben, ob die Schleife komplett ausgeführt oder vorzeitig beendet wurde. Zudem kann der Index abgerufen werden, bei dem die Schleife durch die Methode Break() abgebrochen wurde (siehe weiter unten). Wird eine Schleife per Stop() abgebrochen, so ist der Rückgabewert null.

Schleifenunterbrechung mit ParallelLoopState

Die üblichen For-Schleifen können sich durch den Befehl break selbst vorzeitig beenden. Doch wie kann dieses bei der Methode Parallel.For() umgesetzt werden? Schließlich laufen hier unter Umständen mehrere Threads parallel.

Hierfür bietet die TPL weitere Überladungen der Methode Parallel.For() an, bei denen eine andere Variante des Delegate Action benutzt wird.

public static ParallelLoopResult For(int fromInclusive,
                                     int toExclusive,
                                     Action<int, ParallelLoopState> body);

Neben dem aktuellen Index, enthält der Action-Delegate auch ein Objekt der Hilfsklasse ParallelLoopState.

ClassParallelLoopState

IsExceptional Gibt an, ob es während eines Schleifendurchgangs zu einer unbehandelten Ausnahme gekommen ist.
IsStopped Wird die Methode Stop() aufgerufen, so wird diese Eigenschaft auf true gesetzt. Dieses kann bei verschachtelten Schleifen recht hilfreich sein.
LowestBreakIteration Entspricht der gleichnamigen Eigenschaft der Struktur ParallelLoopResult.
ShouldExitCurrentIteration Diese Eigenschaft wird auf true gesetzt sobald eine Schleife vorzeitig abgebrochen wird. Dabei ist es gleichgültig ob die Schleife per Break(), Stop() oder Cancellation Framework beendet wurde.

Für einen Schleifenabbruch können die Methoden Break() und Stop() herangezogen werden. Welche der beiden eingesetzt wird, hängt von der konkreten Anforderung ab. Denn bei der parallelen Verarbeitung von Schleifen muss einiges beachtet werden.

Angenommen eine parallele Schleife wird durch die Methode Parallel.For() mit 500.000 Durchläufen angelegt. So kann es sein, dass hierfür mehrere Threads angelegt werden. Der 1. Thread könnte dann z.B. für die Iterationen 1 bis 199.999 zuständig sein, während der 2. Thread die Durchläufe 200.000 bis 349.999 und ein 3. Thread von 350.000 bis 500.000 bearbeitet. Würde sich die Schleife selber bei der Iteration 300.000 beenden, so ist nicht vorhersehbar, wie weit der 1. und 3. Thread ihre Bearbeitung durchführen konnten. Genau hier unterscheiden sich die Methoden Break() und Stop().

Durch die Methode Stop() wird die Methode Parallel.For() so früh wie möglich beendet. Dabei wird nicht weiter beachtet, ob alle Schleifendurchläufe bis 300.000 vollständig abgearbeitet wurden.

Bei der Verwendung von Break() wird die Methode so beendet, das alle Iterationen noch beendet werden. In unserem Beispiel also alle bis zum Schleifendurchlauf 300.000. Der 3. Thread kann also unmittelbar beendet werden, während der 1. Thread komplett alle Iteration von 1 bis 199.999 verarbeiten muss.

Mit dem folgenden Beispiel kann recht gut der Unterschied zwischen den Methoden Break() und Stop() gezeigt werden.

public void Run()
{
    Console.WriteLine("Start Run");
    const int arrSize = 500000;
    const int breakIndex = 300000 - 1;
    int[] arr = new int[arrSize];

    ParallelLoopResult loopResult = Parallel.For(0, arrSize, (index, loopState) =>
            {
                DoSomeWork(index);
                arr[index] = 1;
                if (index == breakIndex)
                {
                    Console.WriteLine("Stop/Break");
                    loopState.Break();
                    // loopState.Stop();
                }
            });

    Console.WriteLine("IsCompleted: {0}", loopResult.IsCompleted);
    Console.WriteLine("LowestBreakIteration: {0}", loopResult.LowestBreakIteration);

    int elements = 0;
    for (int i = 0; i <= breakIndex; i++)
        elements += arr[i];
    Console.WriteLine("Elements: {0}", elements);

    elements = 0;
    for (int i = breakIndex + 1; i < arrSize; i++)
        elements += arr[i];
    Console.WriteLine("Elements: {0}", elements);

    Console.WriteLine("End Run");
    Console.ReadLine();
}
private void DoSomeWork(int index)
{
    double temp = 1.1;
    for (int i = 0; i < 100; i++)
        temp = Math.Sin(index) + Math.Sqrt(index) * Math.Pow(index, 3.1415) + temp;
}

Beispiel 3 (Visual Studio 2012)

Abbruch per Break()

Die ersten 300.000 Elemente wurden vollständig bearbeitet. Da die Schleife von mehreren Threads bearbeitet wurde, wurden noch weitere 169.124 Elemente oberhalb von 299.999 bearbeitet.

Picture04

Abbruch per Stop()

Obwohl bei dem Index 299.999 die Schleife beendet wurde, sind nur 295.801 Elemente bearbeitet worden. Die Eigenschaft LowestBreakIteration ist null, da der Abbruch mit Stop() erfolgte.

Picture05

Schleifenabbruch per Cancellation-Framework

Bisher wurde erläutert, wie parallele Schleifen per Stop() und Break() beendet werden. In beiden Fällen wird die parallele Schleife aber von innen heraus beendet. Häufig besteht aber die Forderung, eine Schleife von außen zu beenden.

Genauso wie Parallel.Invoke(), unterstützt auch Parallel.For() das Cancellation-Framework. Das Token wird durch die Struktur CancellationToken abgebildet und durch die Klasse ParallelOptions an die Methode Parallel.For() übergeben. Erzeugt und verwaltet wird das Token durch die Klasse CancellationTokenSource.

Soll die Methode Parallel.For() von außen abgebrochen werden, wird einfach die Methode Cancel() der Klasse CancellationTokenSource aufgerufen. Dieses wird durch das Auslösen der Ausnahme OperationCanceledException bestätigt. Der jeweilige Task wird auf jeden Fall noch beendet, es erfolgt also kein ‘harter’ Abbruch.

In der Klasse ParallelLoopState wird durch einen Abbruch die Eigenschaft ShouldExitCurrentIteration auf true gesetzt. Diese Eigenschaft kann dazu genutzt werden, ein Task evtl. vorzeitig zu beenden und/oder um Ressourcen freizugeben.

public void Run()
{
    Console.WriteLine("Start Run");
    CancellationTokenSource cts = new CancellationTokenSource();
    ParallelOptions po = new ParallelOptions() { CancellationToken = cts.Token };
    Task t = Task.Run(() => Work(po), cts.Token);  // new method in .NET 4.5
    Console.WriteLine("Cancel in 3sec");
    cts.CancelAfter(3000);  // new method in .NET 4.5
    t.Wait();
    Console.WriteLine("End Run");
    Console.ReadLine();
}
private void Work(ParallelOptions po)
{
    Console.WriteLine("Start Work");
    ParallelLoopResult loopResult = new ParallelLoopResult();
    try
    {
        loopResult = Parallel.For(0, 50000, po, (index, loopState) =>
        {
            DoSomeWork(index, loopState);
        });
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("End OperationCanceledException");
    }
    Console.WriteLine("End Work: {0}", loopResult.IsCompleted);
}
private void DoSomeWork(int index, ParallelLoopState loopState)
{
    double temp = 1.1;
    for (int i = 0; i < 5000; i++)
    {
        temp = Math.Sin(index) + Math.Sqrt(index) * Math.Pow(index, 3.1415) + temp;
        if (loopState.ShouldExitCurrentIteration)
        {
            Console.WriteLine("Return DoSomeWork");
            return;
        }
    }
}

Beispiel 4 (Visual Studio 2012)

Sehr häufig findet man Beispiele, bei denen in der Methode Parallel.For() vom CancellationToken die Eigenschaft IsCancellationRequested abgefragt wird. Dieses ist nicht notwendig (die Abfrage wird nie erreicht) und auch nicht ganz richtig. Da über die Klasse ParallelOptions das CancellationToken an die Methode übergeben wird, bricht diese selbständig die Arbeit ab und löst für jeden Thread die Ausnahme OperationCanceledException aus. Diese Ausnahme muss abgefangen werden.

Wichtig ist, dass eine Instanz der Klasse ParallelOptions mit der gesetzten Eigenschaft CancellationToken an die Methode übergeben wird. Nur so wird der Rückgabewert richtig gesetzt und die Methode korrekt abgebrochen.

Picture06

Parallel.ForEach()

Die Methode Parallel.ForEach() ähnelt sehr der Methode Parallel.For(). Daher will ich an dieser Stelle nur ein kurzes Beispiel vorstellen:

public void Run()
{
    Console.WriteLine("Start Run");
    CancellationTokenSource cts = new CancellationTokenSource();
    ParallelOptions po = new ParallelOptions() { CancellationToken = cts.Token };
    Task t = Task.Run(() => Work(po), cts.Token);  // new method in .NET 4.5
    Console.WriteLine("Cancel in 3sec");
    cts.CancelAfter(3000);  // new method in .NET 4.5
    t.Wait();
    Console.WriteLine("End Run");
    Console.ReadLine();
}
private void Work(ParallelOptions po)
{
    Console.WriteLine("Start Work");
    ParallelLoopResult loopResult = new ParallelLoopResult();
    double[] arr = new double[10] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 };
    try
    {
        loopResult = Parallel.ForEach(arr, po, (element, loopState) =>
        {
            Console.WriteLine(element);
            DoSomeWork(element, loopState);
        });
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("End OperationCanceledException");
    }
    Console.WriteLine("End Work: {0}", loopResult.IsCompleted);
}
private void DoSomeWork(double element, ParallelLoopState loopState)
{
    double temp = 1.1;
    for (int i = 0; i < 100000000; i++)
    {
        temp = Math.Sin(element) + Math.Sqrt(element) * Math.Pow(element, 3.1415) + temp;
        if (loopState.ShouldExitCurrentIteration)
        {
            Console.WriteLine("Return DoSomeWork");
            return;
        }
    }
}

Beispiel 5 (Visual Studio 2012)

Unabhängigkeit sicherstellen

Bevor die Klasse Parallel eingesetzt werden kann, müssen alle Abhängigkeiten zwischen den einzelnen Ausführungseinheiten beseitigt werden. Das betrifft insbesondere gemeinsame Variablen oder Objekte, auf die schreibend zugegriffen wird. Diese müssen mit geeigneten Mittel gegen zeitgleiche Zugriffe geschützt werden. Nur so kann eine fehlerfreie Ausführung sicher gestellt werden.

public void Run()
{
    Console.WriteLine("Start Run");
    const int iAmount = 100000;

    // sequenziell
    int[] iValuesSeq = new int[iAmount];
    iValuesSeq[0] = 1;
    for (int i = 1; i < iAmount; i++)
    {
        iValuesSeq[i] = iValuesSeq[i - 1] + 1;
    }

    // parallel
    int[] iValuesPar = new int[iAmount];
    iValuesPar[0] = 1;
    Parallel.For(1, iAmount, i =>
        {
            iValuesPar[i] = iValuesPar[i - 1] + 1;
        });

    // Test
    long lSumSeq = 0;
    long lSumPar = 0;
    for (int i = 0; i < iAmount; i++)
    {
        lSumSeq += iValuesSeq[i];
        lSumPar += iValuesPar[i];
    }
    Console.WriteLine("Seq: {0}  Par: {1}", lSumSeq, lSumPar);

    Console.WriteLine("End Run");
    Console.ReadLine();
}

Beispiel 6 (Visual Studio 2012)

Die Methode Parallel.For() berechnet ein fehlerhaftes Ergebnis:

Picture07

Da sich aber die notwendigen Sperren sehr negativ auf das Laufzeitverhalten auswirken, sollte der verwendete Algorithmus so angepasst werden, das nur noch möglichst wenig gemeinsame Variablen vorhanden sind.

Beurteilung der Performanzänderungen

Wurde eine Aufgabe unter Berücksichtigung paralleler Aspekte umgesetzt, stellt sich schnell die Frage nach dem Nutzen. Wie kann die erstellte Lösung beurteilt werden? Hierzu gibt es einige Faktoren und Verfahren, die zur Auswertung herangezogen werden können.

Speedup

Der Speedup lässt sich recht einfach berechnen, da dieser das Verhältnis zwischen der parallelen und der sequenziellen Ausführungszeit beschreibt. Hierzu wird die Zeitdauer auf dem Einkernsystem durch die Zeitdauer des Mehrkernsystems geteilt:

Speedup

T1 Ausführungsdauer auf einem Einkernsystem.
Tp Ausführungsdauer auf einem Mehrkernsystem. Dabei entspricht p der Anzahl der CPU-Kerne.

S < 1

Die Lösung läuft auf einem Mehrkernsystem schlechter als auf einem Einkernsystem. Kann auch nach intensiver Analyse die Ursache nicht beseitigt werden, sollte der Einsatz einer sequenziellen Lösung in Betracht gezogen werden.

S < p

In diesem Fall erhöht sich zwar die Performanz mit der Anzahl der CPU-Kerne, doch nicht im gleichen Maße, wie auch die CPU-Kerne erhöht werden. Eine Verdoppelung der CPU-Kerne bedeutet lange noch nicht eine Halbierung der Ausführungszeit. Dieser Verlauf wird auch als Sublinearer Speedup bezeichnet und tritt meiner Erfahrung nach am häufigsten auf. Auch wenn sich dieses etwas enttäuschend anhört, so können sie mit einem sublinearen Speedup zufrieden sein. Ein Intel-Mitarbeiter (Arch D. Robison) schrieb hierzu eine kurze Anmerkung in seinen Blog: Don’t worry about sublinear speedup, be happy!.

S = p

Ist der Speedup gleich der Anzahl der CPU-Kerne, so skaliert die Lösung sehr gut. Da sich die Ausführungszeit linear zur der Anzahl der CPU-Kerne verhält, wird der Begriff Linearer Speedup verwendet.

S > p

Kommt in der Praxis selten vor (genaugenommen habe ich es noch nie gesehen) und wird als Superlinear Speedup bezeichnet.

Zur genauen Analyse einer Lösung empfiehlt sich das Erstellen einer Messreihe, in der der Speedup für eine unterschiedliche Anzahl von CPU-Kernen erstellt wird. Hilfreich ist hierbei die Eigenschaft MaxDegreeOfParallelism der Klasse ParallelOptions (siehe oben). Die Anzahl der CPU-Kerne kann hierdurch in gewisser Weise simuliert werden.

Auf diese Weise lässt sich ein Diagramm erstellen, in der die Tendenz des Speedup erkennbar ist. Schließlich muss der Speedup nicht linear sein. Je nach Anzahl der CPU-Kerne kann sich dieser zum Guten oder zum Schlechten wenden.

Effizienz

Zur Beurteilung einer Lösung wird neben dem Speedup auch gerne die Effizienz angegeben. Hierbei wird der Speedup durch die Anzahl der CPU-Kerne dividiert.

Effizienz

Beispiel:

Tabelle01

Amdahlsches Gesetz

Gene Amdahl hat sich mit der Frage auseinander gesetzt, wie der Speedup eines Systems vorherbestimmt werden kann. So zeigt das Gesetz sehr gut, dass durch eine Erhöhung der CPU-Kerne gegen Unendlich nicht automatisch die Laufzeit gegen 0 geht. Jede Anwendung enthält sequenzielle Anteile, die sich nicht parallelisieren lassen. Der sich daraus ergebene maximale Speedup kann durch das Amdahlsche Gesetz (grob) berechnet werden.

Bei Wikipedia ist zu dem Thema eine sehr gute Beschreibung verfügbar (Amdahlsches Gesetz). Allen, die sich für dieses Thema tiefgehender interessieren, kann ich den Artikel sehr empfehlen.

Advertisements
  1. No comments yet.
  1. November 19, 2013 at 10:05 pm

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: