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

TPL Teil 2 – Die Klasse Task

Bisher stand bei der Programmierung von multithreading Applikationen die Klasse Thread im Mittelpunkt. Mit .NET 4 wurde die Task Parallel Library (TPL) eingeführt, die mit der Klasse Task ein neues Konzept zur Verfügung stellt. Diese ist zwar auf den ersten Blick vergleichbar mit der Thread-Klasse, bringt aber einige Neuerungen und Verbesserungen mit sich.

Im ersten Teil wurde die Klasse Task schon kurz vorgestellt. Dort wurde allerdings nur gezeigt, wie mit Hilfe eines Task-Objektes Code ausgeführt werden kann. Auch wurden einfache Methoden zur Synchronisierung vorgestellt. Doch bietet die Klasse Task deutlich mehr Möglichkeiten.

Task-Objekt anlegen

per new-Operator

Um ein Task-Objekt nutzen zu können, muss zuerst eine Instanz angelegt werden. Dieses kann in bekannter Weise mit dem new-Operator erfolgen. Der Konstruktor ist mehrfach überladen. In der einfachsten Form erwartet dieser ein Delegate vom Typ Action.

public void Run()
{
    Task task1 = new Task(TaskMethod01);
    task1.Start();
    Console.ReadLine();
}
public void TaskMethod01()
{
    Console.WriteLine("TaskMethod01");
    Thread.Sleep(1000);
}

Mit der Methode Start() wird der Task gestartet. Genauer gesagt wird der Code für die Ausführung bei dem Task-Scheduler vorgesehen. Wann der Task zur Ausführung kommt, hängt u.a. von der Auslastung des Systems ab. Der Aufruf der Methode ist asynchron, der Aufrufer wird somit nicht blockiert.

Der Delegate Action kann auch als Lambda-Ausdruck angegeben werden. Dadurch wird der Code bei kleineren Methoden kompakter und übersichtlicher.

Task task1 = new Task(() =>
    {
        Console.WriteLine("TaskMethod01");
        Thread.Sleep(1000);
    });
task1.Start();
Console.ReadLine();

Des Weiteren stehen noch verschiedene Überladungen zur Verfügung:

public Task(Action action);
public Task(Action action, CancellationToken cancellationToken);
public Task(Action action, TaskCreationOptions creationOptions);
public Task(Action action, CancellationToken cancellationToken, TaskCreationOptions creationOptions);
CancellationToken

Mit der TPL wurde auch eine leistungsfähige Infrastruktur zum kontrollierten Beenden von Tasks eingeführt. Hierbei wird der Abbruchwunsch durch ein Token an den Task übergeben. Das Token wird durch die Struktur CancellationToken abgebildet und an den Task übergeben. Erzeugt und verwaltet wird das Token durch die Klasse CancellationTokenSource.

Soll ein Task von außen abgebrochen werden, wird einfach die Methode Cancel() der Klasse CancellationTokenSource aufgerufen. In den Task muss zyklisch die Eigenschaft IsCancellationRequested abgefragt werden. Ist diese auf true, so wurde das Beenden des Task angefordert. Dieses wird durch das Auslösen der Ausnahme OperationCanceledException bestätigt. Der Abbruch erfolgt kooperativ. Der Task entscheidet selber, wann genau dieser beendet wird. Somit können alle notwendigen Aufräumarbeiten (z.B. Locks freigeben) durchgeführt werden. Das Abfragen der Eigenschaft IsCancellationRequested und Auslösen der Exception kann durch die Methode ThrowIfCancellationRequested() ersetzt werden.

public void Run()
{
    CancellationTokenSource cts = new CancellationTokenSource();
    CancellationToken ct = cts.Token;
    Console.WriteLine("Run Start");
    Task task1 = new Task((ct1) =>
        {
            CancellationToken ctLocal = (CancellationToken)ct1;
            Console.WriteLine("Task Start");
            try
            {
                while (true)
                {
                    Console.Write(".");
                    ctLocal.ThrowIfCancellationRequested();
                    Thread.Sleep(1000);
                }
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine("OperationCanceledException");
            }
            Console.WriteLine("Task End");
        }, ct);
    task1.Start();
    Console.ReadLine();
    cts.Cancel();
    task1.Wait();
    Console.WriteLine("Run End");
    Console.ReadLine();
}

Beispiel 1 (Visual Studio 2010)

Allein das Token reicht nicht aus, eine Task zu beenden. Das Beenden kann immer nur über eine Instanz der Klasse CancellationTokenSource erfolgen. Dadurch kann das Token ohne Bedenken an fremde Klassen weitergeleitet werden.

TaskCreationOptions

Das Erzeugen einer Task kann durch die Aufzählung TaskCreationOptions beeinflusst werden.

TaskCreationOptions.None Standardeinstellung.
TaskCreationOptions.PreferFainess Es wird versucht, Tasks, die als erstes gestartet wurden, auch als erstes auszuführen.
TaskCreationOptions.LongRunning Diese Option ist speziell für Tasks mit langer Laufzeit.
TaskCreationOptions.AttachedToParent Verbindet ein Child-Task mit einem Parent-Task. Der Parent-Task ist erst dann beendet, wenn alle Child-Tasks beendet sind.

Das folgende Beispiel zeigt die Verwendung von TaskCreationOptions.AttachedToParent:

Task task1 = new Task(() =>
{
    Console.WriteLine("Task01 Start");
    Task task2 = new Task(() =>
    {
        Console.WriteLine("Task02 Start");
        Thread.SpinWait(50000000);
        Console.WriteLine("Task02 End");
    }, TaskCreationOptions.AttachedToParent);
    task2.Start();
});
task1.Start();
task1.Wait();
Console.WriteLine("Task01 End");

Innerhalb des Delegates von task1 wird task2 erzeugt. Ohne die Angabe von TaskCreationOptions.AttachedToParent sind beide Tasks voneinander unabhängig.

Picture01 Mit der Ausführung von task1 wird task2 erzeugt und gestartet. Während task2 noch aktiv ist, ist task1 schon beendet.

Wird TaskCreationOptions.AttachedToParent bei der Erzeugung von task2 angegeben, so ändert sich der Ablauf.

Picture02 Der innere Task (task2) hat mit der äußeren Task (task1) eine Parent-Child Beziehung.
task1 ist erst dann beendet, wenn task2 beendet ist.

Diese Option ist u.U. sehr hilfreich bei der Synchronisierung. Ein Parent-Task erzeugt mehrere Child-Tasks. Erst wenn alle Child-Tasks beendet sind, ist auch der Parent-Task beendet.

Beispiel 2 (Visual Studio 2010)

per Factory-Klasse

Zur Erzeugung und zum Starten eines Task-Objektes kann auch die Methode StartNew() der Klasse TaskFactory genutzt werden. Um die Handhabung zu vereinfachen, besitzt die Klasse Task die statische Eigenschaft Factory vom Typ TaskFactory. Auch entfällt bei dieser Variante der Aufruf von Start(). Somit ist das Erzeugen und Starten eines neuen Task-Objektes denkbar einfach.

Console.WriteLine("Run Start");
Task task1 = Task.Factory.StartNew(() =>
    {
        Console.WriteLine("Task Start");
        Thread.SpinWait(50000000);
        Console.WriteLine("Task End");
    });
task1.Wait();
Console.WriteLine("Run End");
Console.ReadLine();

Auch diese Methode ist mehrfach überladen. CancellationToken und TaskCreationOptions werden auf die gleiche Weise angewendet, wie bei dem Konstruktor von Task.

Ein weiterer Vorteil der Klasse TaskFactory ist das Definieren von Workflows. Dieses werde ich in einem späteren Post gesondert behandeln.

Seiteneffekte

Allerdings muss an dieser Stelle auch auf mögliche Seiteneffekte hingewiesen werden. Diese treten auf, wenn aus dem Hauptthread und aus dem Task auf gemeinsame Objekte zugegriffen wird. Hierzu ein einfaches Beispiel.

int taskPara = 10;
Console.WriteLine(taskPara);
Task task1 = Task.Factory.StartNew(() => { taskPara++; });
Console.WriteLine(taskPara);
Thread.Sleep(500);
Console.WriteLine(taskPara);
task1.Wait();
Console.ReadLine();

Wie wird die Ausgabe des Programms aussehen? Die erste Ausgabe ist auf jeden Fall 10, da die Variable mit 10 initialisiert und direkt ausgegeben wird. Anschließend wird der Task gestartet und innerhalb der Task wird die Variable um 1 erhöht. Somit würde man erwarten, dass die zweite Ausgabe 11 sein sollte. Diese ist aber ebenfalls 10.

Die Methode StartNew() der Klasse Factory initialisiert einen Task und versucht, diesen unmittelbar zu starten. Dieser Vorgang dauert aber einen Moment und wird asynchron zum Hauptthread ausgeführt. Während dieser Zeit erfolgt die Ausgabe in Zeile 4 noch mit dem Wert 10. Durch den Sleep() in Zeile 5 ist ausreichend Zeit vorhanden, um den Task zu starten und auszuführen. Somit wird erst in Zeile 6 die 11 ausgegeben. Wie schnell ein Task zur Ausführung kommt ist nicht vorhersehbar und ist von mehreren Faktoren, wie z.B. der Systemauslastung, abhängig.

Zustände

Sobald ein Task-Objekt angelegt wurde, kann dieses verschiedene Zustände annehmen. Über die Eigenschaft Status kann dieser Zustand ermittelt werden. Die Eigenschaft ist eine Aufzählung vom Typ TaskStatus.

Created Der Task wurde initialisiert, aber noch nicht gestartet. Dieser Zustand ist solange aktiv, bis die Methode Start() aufgerufen wurde.
WaitingForActivation Der Task wartet auf die Aktivierung. Der Task wurde noch nicht in die interne Ausführungswarteschlange eingetragen.
WaitingToRun Der Task wartet darauf, ausgeführt zu werden. Tasks die mittels TaskFactory.StartNew() erzeugt wurden, starten mit diesem Zustand.
Running Der Task läuft, ist aber noch nicht beendet.
WaitingForChildrenToComplete Der Task wurde erfolgreich ausgeführt und wartet auf das Beenden seiner Child-Tasks.
RanToCompletion Der Task wurde erfolgreich (ohne Abbruch und ohne Exception) ausgeführt und beendet.
Canceled Der Task wurde vorzeitig abgebrochen (siehe CancellationToken weiter oben).
Faulted Der Task, oder eines der Child-Tasks, wurde wegen einer Exception beendet.

Daten an ein Task-Objekt übergeben

Task-Objekte leben nicht immer isoliert voneinander. Es muss möglich sein, Werte an eine Task-Methode zu übergeben. Hierzu wurde die Methode StartNew() entsprechend überladen:

public Task StartNew(Action<Object> action, Object state)

Der Konstruktor der Klasse Task ist ebenfalls überladen:

public Task(Action<Object> action, Object state)

Der erste Parameter ist ein Delegate vom Typ Action<Object>. Ein Delegate also, der einen Parameter vom Typ Object erwartet und keinen Rückgabewert besitzt. Der zweite Parameter ist das Objekt, das bei dem Aufruf des Delegate an die Methode übergeben wird.

Sollen mehrere Werte übergeben werden, so kann für die Parameterübergabe eine Struktur genutzt werden, die alle gewünschten Werte enthält. In diesem Beispiel ist es die Struktur TaskPara.

class Program
{
    public struct TaskPara
    {
        public string Name;
        public int Age;
    }
    static void Main(string[] args)
    {
        new Program().Run();
    }
    public void Run()
    {
        Console.WriteLine("Run Start");
        TaskPara taskPara;
        taskPara.Name = "Karl";
        taskPara.Age = 30;
        Task task1 = Task.Factory.StartNew(new Action<Object>(TaskMethod),
                                                                 taskPara);
        task1.Wait();
        Console.WriteLine("Run End");
        Console.ReadLine();
    }
    public void TaskMethod(Object para)
    {
        Console.WriteLine("Name: {0}  Age: {1}", ((TaskPara)para).Name,
                                                 ((TaskPara)para).Age);
    }
}

Wird der Konstruktor der Klasse Task eingesetzt, so sieht die Benutzung ähnlich aus, wie bei der Methode StartNew().

Task task1 = new Task(new Action<Object> (TaskMethod), taskPara);
task1.Start();

Dank anonymer Methoden, Lambda-Ausdrücken und Inference (Typrückschluss) gibt es für den Aufruf des Delegate mehrere Schreibweisen. Wer mehr über Lambda-Expressions erfahren will, kann dieses in einem anderen Post von mir nachlesen. Dort habe ich das Thema ausführlich behandelt.

Task task1 = Task.Factory.StartNew(TaskMethod, taskPara);
Task task1 = Task.Factory.StartNew((para) => TaskMethod(para), taskPara);
Task task1 = Task.Factory.StartNew(delegate(object para)
   {
       Console.WriteLine("Name: {0}  Age: {1}", ((TaskPara)para).Name,
                                                ((TaskPara)para).Age);
   }, taskPara);
Task task1 = Task.Factory.StartNew((para) =>
   {
       Console.WriteLine("Name: {0}  Age: {1}", ((TaskPara)para).Name,
                                                ((TaskPara)para).Age);
   }, taskPara);

Wer bisher mit der Klasse Thread gearbeitet hat, wird die Ähnlichkeit mit der Klasse ParameterizedThreadStart wiedererkennen. Dort wird ebenfalls eine Thread-Methode mit einem Parameter vom Typ Object verwendet. Angenehm bei der Klasse Task, ist die etwas kürzere Schreibweise.

Beispiel 3 (Visual Studio 2010)

Rückgabewerte

Die Klasse Thread besitzt keine Möglichkeit, Werte sicher und einfach zurückzugeben. Ein übliches Verfahren, um Rückgabewerte aus einem Thread zu bekommen, ist die Benutzung eines gemeinsamen Objektes. Dieses Objekt wird an den Thread übergeben und kann von diesem verändert werden. Ist der Thread beendet, so stehen die veränderten Werte des Objektes dem Hauptthread zur Verfügung. Diese Methode ist nicht ganz ungefährlich, da durch die Übergabe einer Objektreferenz an einen Thread Seiteneffekte auftreten können. Daher sollten zeitgleiche Zugriffe immer durch entsprechende Synchronisierungsmethoden (z.B. Locks) verhindert werden. Des Weiteren muss erkannt werden, ob der Thread beendet wurde und die Rückgabedaten gültig sind. Somit ist bei der Verwendung von reinen Threads ein erhöhter Aufwand notwendig. Mit der Klasse Task können Rückgabewerte deutlich einfacher abgefragt werden.

Für solche Aufgaben wird die Klasse Task<TResult>, die von Task abgeleitet ist, benutzt. Der generische Typparameter beschreibt den Datentyp des Rückgabewertes. Außerdem besitzt die Klasse die Eigenschaft Result. Diese besitzt einen internen Synchronisierungsmechanismus, der bei einem Zugriff auf die Eigenschaft Result erst dann den Wert zurückliefert, wenn der Task fertig ist. Ist dieser noch nicht beendet, so wird auf die Fertigstellung gewartet.

class Program
{
    public struct TaskPara
    {
        public string Name;
        public int Age;
    }

    static void Main(string[] args)
    {
        new Program().Run();
    }
    public void Run()
    {
        Console.WriteLine("Run Start");
        TaskPara taskPara;
        taskPara.Name = "Karl";
        taskPara.Age = 30;
        Task<string> task1 = Task<string>.Factory.StartNew((para) =>
            {
                Thread.SpinWait(100000000);
                return string.Format("{0} is {1} years old.",
                                              ((TaskPara)para).Name,
                                              ((TaskPara)para).Age);
            }, taskPara);
        Console.WriteLine("Result: {0}", task1.Result);
        Console.WriteLine("Run End");
        Console.ReadLine();
    }
}

Das vorherige Beispiel wurde so erweitert, das der Task einen String zurückliefert. Das Ergebnis wird erst dann ausgegeben, wenn der Task beendet ist. Die Methode SpinWait() dient in diesem Beispiel dazu, etwas Rechenzeit zu simulieren. Auf das Warten durch die Methode Wait() kann im Hauptthread verzichtet werden, da das Synchronisieren von der Klasse Task<TResult> übernommen wird.

class Program
{
    public struct TaskPara
    {
        public string Name;
        public int Age;
    }

    static void Main(string[] args)
    {
        new Program().Run();
    }
    public void Run()
    {
        Console.WriteLine("Run Start");
        TaskPara taskPara;
        taskPara.Name = "Karl";
        taskPara.Age = 30;
        Task<string> task1 = Task<string>.Factory.StartNew((para) =>
            {
                Thread.SpinWait(100000000);
                return string.Format("{0} is {1} years old.",
                                                       ((TaskPara)para).Name,
                                                       ((TaskPara)para).Age);
            }, taskPara);
        Console.WriteLine("Result: {0}", task1.Result);
        Console.WriteLine("Run End");
        Console.ReadLine();
    }
}

Der typisierte Rückgabewert und die eingebaute Synchronisierung machen die Benutzung der Klasse Task sehr angenehm und der Quelltext ist leicht lesbar.

Beispiel 4 (Visual Studio 2010)

Auf das Beenden einer oder mehrerer Tasks warten

Oftmals ist es notwendig auf das Beenden einer oder mehrerer Tasks zu warten, bevor mit der weiteren Programmausführung fortgefahren werden kann. Die einfachste Möglichkeit ist der Aufruf der Methode Wait() eines Task-Objektes. Die Methode ist mehrfach überladen.

public void Wait();
public bool Wait(int millisecondsTimeout);
public bool Wait(TimeSpan timeout);
public void Wait(CancellationToken ct);
public bool Wait(int millisecondsTimeout, CancellationToken ct);

Ohne Parameter wird unendlich lange auf das Beenden des Task gewartet und somit die Programmausführung blockiert. Alternativ kann eine maximale Zeitdauer angegeben werden. Endweder in Millisekunden oder als TimeSpan-Objekt. Ist der Task vor Ablauf der angegebenen Zeit beendet, so liefert Wait() true zurück.

Die Struktur CancellationToken wurde schon weiter oben angesprochen. Diese dient dazu, das Beenden eines Task von außen anzufordern. Ebenfalls kann die Struktur genutzt werden, um das Warten vorzeitig zu beenden.

Hierzu ein Beispiel. Eine Anwendung erzeugt zwei Tasks. Die äußere Task (task1) erzeugt eine weitere Task, die innere Task (task2). Die innere Task führt eine Aktion aus, die in diesem Beispiel auf Grund eines Programmfehlers nie beendet wird. Die äußere Task wartet auf das Beenden der inneren Task. Beim Beenden der Anwendung soll das Warten der äußeren Task kontrolliert abgebrochen werden. Somit bekommt die äußere Task Gelegenheit, wichtige Ressourcen (z.B. Locks) freizugeben.

CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;
Console.WriteLine("Start Run");
Task task1 = Task.Factory.StartNew((ct1) =>
{
    Console.WriteLine("Start Task 1");
    CancellationToken ct1Local = (CancellationToken)ct1;
    Task task2 = Task.Factory.StartNew(() =>
    {
        Console.WriteLine("Start Task 2");
        while (true)
        {
            Console.Write(".");
            Thread.Sleep(1000);
        }
        Console.WriteLine("End Task 2");
    });
    try
    {
        task2.Wait(ct1Local);
    }
    catch { }
    Thread.SpinWait(50000000); // relase resources
    Console.WriteLine("End Task 1");
}, ct);
Console.ReadLine();
cts.Cancel();
task1.Wait();
Console.WriteLine("End Run");
Console.ReadLine();

Hierzu wird die Struktur CancellationToken an den äußeren Task übergeben und bei der Methode Wait() als Parameter benutzt (Zeile 20). Somit wird durch den Aufruf der Methode Cancel() in Zeile 27 das Warten auf den inneren Task vorzeitig abgebrochen.

Beispiel 5 (Visual Studio 2010)

Die Klasse Task besitzt des Weiteren die statischen Methoden WaitAll() und WaitAny(). Ähnlich der Methode Wait(), besitzen diese vergleichbare Überladungen:

public static void WaitAll(params Task[] tasks);
public static void WaitAll(Task[] tasks, CancellationToken ct);
public static bool WaitAll(Task[] tasks, int millisecondsTimeout);
public static bool WaitAll(Task[] tasks, TimeSpan timeout);
public static bool WaitAll(Task[] tasks, int millisecondsTimeout, CancellationToken ct);

public static int WaitAny(params Task[] tasks);
public static int WaitAny(Task[] tasks, CancellationToken ct);
public static int WaitAny(Task[] tasks, int millisecondsTimeout);
public static int WaitAny(Task[] tasks, TimeSpan timeout);
public static int WaitAny(Task[] tasks, int millisecondsTimeout, CancellationToken ct);

Während Task.WaitAll() erst dann mit der Programmausführung fortfährt, wenn alle übergebenen Tasks beendet sind, so wird bei Task.WaitAny() fortgefahren, sobald einer der Task beendet wurde.

Der erste Parameter ist immer eine Parameterliste von Task-Objekten. Alle weiteren Parameter haben die gleiche Bedeutung wie bei der Methode Wait(). Somit kann das Warten zeitlich begrenzt oder vorzeitig abgebrochen werden. Wird kein Parameter übergeben, so wird die Methode direkt wieder verlassen.

Die Methode Task.WaitAll() hat die gleichen Rückgabewerte wie Wait(). Dagegen liefert Task.WaitAny() immer den Index des jeweiligen Tasks zurück, der beendet wurde. Hiermit ist der Index innerhalb der Parameterliste gemeint; das erste Task-Objekt hat den Index 0. Wird das Warten vorzeitig beendet, weil z.B. die maximale Wartezeit überschritten wurde, so liefert die Methode –1 zurück. Hier einige Beispiele:

Task task1 = Task.Factory.StartNew(() =>
{
    Thread.Sleep(1000);
});
Task task2 = Task.Factory.StartNew(() =>
{
    Thread.Sleep(2000);
});
Task.WaitAny(new Task[] { task1, task2 }, 1000);
Task.WaitAll(task1, task2);
Task.WaitAll(new Task[] { task1, task2 });

Auswirkung auf die Performanz

Neben der einfachen Handhabung, bietet die Klasse Task auch Potenzial, die Performanz einer Anwendung zu steigern. Hierzu habe ich in einer einfachen Anwendung die Klasse Thread mit der Klasse Task verglichen. In der Anwendung habe ich eine Methode definiert, die sinnlose Berechnungen durchführt. Diese Methode soll 50mal ausgeführt werden. Welche Möglichkeiten stehen zur Verfügung, um dieses möglichst effektiv umzusetzen?

private void DoSomething(object o)
{
    ManualResetEvent resetEvent = null;
    if (o != null) resetEvent = (ManualResetEvent)o;
    double x = 0;
    for (int a = 0; a < 1000000; a++)
        x = Math.Log(a) / Math.Sqrt(a - Math.Sin(x));
    if (resetEvent != null) resetEvent.Set();
}

Die Klasse ManualResetEvent dient nur dazu, das Beenden der Berechnung zu signalisieren.

sequenzielle Ausführung

Die 50 Methodenaufrufe erfolgen sequenziell. Es ist zu erwarten, dass dieses die langsamste Variante darstellt. Der einzige Vorteil, ist die einfache Implementierung.

private void CalcBySequential()
{
    Stopwatch watch = Stopwatch.StartNew();
    for (int n = 0; n < 50; n++)
    {
        DoSomething(null);
    }
    watch.Stop();
    Console.WriteLine("Sequential: {0}ms.", watch.ElapsedMilliseconds);
}

Verwendung der Klasse Thread

Da in jedem modernen Rechner, eine Multi-Core-CPU zum Einsatz kommt, liegt es nahe, die Berechnungen auf mehrere Threads zu verteilen. Das Betriebssystem sorgt dafür, dass die einzelnen Threads auf die einzelnen CPU-Kerne verteilt werden.

private void CalcByThread()
{
    WaitHandle[] waitHandle = new WaitHandle[50];
    Stopwatch watch = Stopwatch.StartNew();
    for (int n = 0; n < 50; n++)
    {
        ManualResetEvent resetEvent = new ManualResetEvent(false);
        waitHandle[n] = resetEvent;
        Thread thread = new Thread(DoSomething);
        thread.Start(resetEvent);
    }
    WaitHandle.WaitAll(waitHandle);
    watch.Stop();
    Console.WriteLine("Thread: {0}ms.", watch.ElapsedMilliseconds);
}

Verwendung der Klasse Task

Ähnlich ist der Ansatz mit der Klasse Task. Einfacher ist hier das Synchronisieren mit dem Haupt-Thread. Separate WaitHandle-Objekte sind nicht notwendig.

private void CalcByTask()
{
    Task[] tasks = new Task[50];
    Stopwatch watch = Stopwatch.StartNew();
    for (int n = 0; n < 50; n++)
    {
        tasks[n] = Task.Factory.StartNew(DoSomething, null);
    }
    Task.WaitAll(tasks);
    watch.Stop();
    Console.WriteLine("Task: {0}ms.", watch.ElapsedMilliseconds);
}

Die sequenzielle Ausführung dient immer als Vergleich und wird mit 100% angegeben. Des Weiteren habe ich das Programm auf drei unterschiedlichen Rechnern ausgeführt.

Intel Celeron 2 GHz (1 CPU-Kern)

Tabelle01

Intel Core-Duo 1,8 GHz (2 CPU-Kerne)

Tabelle02

Intel Core i7 3,4 GHz (8 CPU-Kerne)

Tabelle03

Auf den beiden Multi-Core-CPUs ist gut zu erkennen, dass die Klasse Task die größte Steigerung mit sich bringt. Das ist dadurch zu erklären, dass nicht unmittelbar mit der Methode Start() der Klasse Task ein Thread gestartet wird. Stattdessen werden die Threads erst bei Bedarf erzeugt und dann den einzelnen Task-Instanzen zugeordnet. Dadurch werden, in Abhängigkeit der verfügbaren CPU-Kerne, nicht mehr Threads angelegt, als effektiv verarbeitet werden können.

Auf der Single-Core-CPU verursachen die vielen Threads eine Verringerung der Performanz. Schließlich muss das Betriebssystem ständig zwischen den einzelnen Threads umschalten. Hier macht sich die TPL ebenfalls positiv bemerkbar. Diese erkennt, dass nur ein CPU-Kern vorhanden ist und erzeugt dementsprechend wenige Threads.

Mit der Anzahl der CPU-Kerne nimmt auch der Geschwindigkeitsvorteil der Klasse Task gegenüber der Klasse Thread zu (55% bei 2 CPU-Kernen und 27% bei 8 CPU-Kernen).

Beispiel 6 (Visual Studio 2010)

Weitere Beispiele

Wer sich für weitere Beispiele interessiert, dem sei die Seite ‘Sample for Parallel Programming with the .NET Framework’ von Microsoft empfohlen. Die Beispiele decken nicht nur die TPL, sondern auch Themen wie PLINQ oder thread-safed Collections ab.

Advertisements
  1. No comments yet.
  1. November 12, 2012 at 9:14 pm
  2. September 11, 2013 at 9:46 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: