System.Threading.Tasks Basics Teil 1

Einleitung

In diesem Artikel geht es über Tasks aus dem System.Threading.Tasks Namespace. Tasks wurden mit dem .net Framework 4.0 als Teil der Task Parallel Library (TPL) eingeführt und mit 4.5 erweitert und verbessert. In diesem Artikel werde ich nur den Stand Framework 4.5 behandeln die Meisten Dinge funktionieren aber auch mit dem .net 4.0 Framework.

Was sind Tasks und wozu brauche ich sie?

Tasks helfen einem bei der asynchronen Programmierung. Man kann sie überall einsetzen wenn man etwas im Hintergrund oder gleichzeitig (parallel) machen will. Vor der TPL gab es Threads diese gibt es natürlich immer noch.

Das Rad wird natürlich nicht neu erfunden und daher wird es kaum jemanden wundern das Tasks auf Threads basieren. Genauer gesagt verwendet Tasks Threads aus dem Threadpool. Auf den Threadpool werde ich hier nur kurz eingehen kurz zusammengefasst ist der Threadpool eine „Sammlung“ von vorbereiteten Threads die wiederverwendet werden können. Da die Threads nicht neu erstellt werden ist es wesentlich schneller einen Thread aus dem Pool zu nehmen als einen neuen zu erstellen. Die Minimum und Maximum Anzahl von Threads im Pool ist dynamisch und von mehreren Faktoren, hauptsächlich aber von der CPU Kernanzahl abhängig.

Tasks sind nicht nur schneller erstellt als Threads es gibt auch viele Komfort Funktionen wie das Abbrechen von Tasks das Aneinander ketten oder warten auf mehrere Tasks. Und um genau diese Dinge geht es in diesem Artikel.

Task Erstellung

Einen Task zu erstellen ist recht einfach man braucht einen Delegate zu der Methode die er im Hintergrund ausführen soll.

Vereinfacht geschrieben sieht das dann so aus

Task t = new Task(MethodenName);

Wenn es sich um sehr kurze Aufrufe handelt verwendet man üblicherweise den System.Action delegate bzw. Func<TResult> wenn der Task auch etwas zurückliefern soll.

Den Task kann man dann einfach starten

t.Start();

und er beendet sich selbst wenn er fertig ist.

Will man den Task gleich starten gibt es 2 Kurzschreibweisen:

Um einen Task ohne Rückgabe wert zu starten

//Eine Methode aufrufen

Task t1 = Task.Factory.StartNew(MethodenName);

//Oder wenn es nicht viel Code ist einfach gleich direkt

Task t2 = Task.Factory.StartNew(() =&gt;
{
	for (int i = 0; i &lt;= 10000; i++)
	{
   		Debug.WriteLine( DateTime.Now.ToString(&quot;O&quot;) + &quot; --&gt; Ich laufe im Hintergrund &quot;);
       Thread.Sleep(100);                    
	}
 });

Wenn man einen Wert von dem gestarteten Task zurück bekommen will dann kann man ihn mit Run aufrufen

Task&lt;string&gt; t = Task.Run(new Func&lt;string&gt;(MethodenNameMitReturn));

            AndererCode();

            string result = t.Result;

Und noch ein Beispiel mit Ein und Ausgabe Parameter:

#region Input und Output Parameter

var irgendeinInputObject = new List&lt;double&gt; { 1.35, 13.76 };
Task&lt;double&gt; t4 = Task.Factory.StartNew&lt;double&gt;(input =&gt;
{
    // input ist immer vom Typ object und 
    // muss daher auf das passende object gecastet werden.
    List&lt;double&gt; i = input as List&lt;double&gt;; 

    /*...Code der im Hintergrund laufen soll */
    return i.Average();
}, irgendeinInputObject);

//... Code der im aktuellen Thread (Oft im UI Thread) l&#228;uft.
AndererCode();

double result2 = t4.Result;
Console.WriteLine(&quot;Task Resultat:&quot; + result2.ToString(&quot;n4&quot;));
#endregion

Jedem der den Code Aufmerksam gelesen hat müsste bei dieser Zeile etwas auffallen:

string result = t.Result;

Zuerst behaupte ich das der Thread in dem der Task aufgerufen wird einfach weiter ausgeführt wird, der Task selbst in einem eigenen Thread läuft, und trotzdem greife ich direkt auf das Ergebnis des Threads zu. Das kann ja gar nicht funktionieren…

Wären t3 ein simpler Thread dann wäre das auch richtig so und die Applikation würde meistens nicht das tun was wir erwarten. Da t3 aber ein Task ist sieht die ganze Sache gleich anders aus. Wenn man bei einem Task auf das Ergebnis zugreift dieser Task aber noch nicht beendet ist wird automatisch ein Wait() aufgerufen. Der aktuelle Thread wird dadurch so lange angehalten bis der Task beendet ist. Mehr dazu etwas später in diesem Beitrag

Der Aktuelle Zustand eines Tasks

Die Task Klasse hat einige Properties die Auskunft über den aktuellen Zustand eines Tasks geben. Zum einen gibt es den Status vom Typ TaskStatus dieser liefert detailliert den aktuellen Status des Tasks über den gesamten Lebenszyklus. Für die Meisten Anwendungsfälle ist das aber schon zu viel und verwirrt am Anfang mehr als es hilft. Viel Interessanter sind die 3 Boolean Properties:

  • IsCompleted: Gibt an ob der Task abgeschlossen ist. Achtung auch ein fehlgeschlagener oder abgebrochener Task ist abgeschlossen.
  • IsFaulted: Gibt an ob es während der Ausführung eine unbehandelte Ausnahme (Exception) gegeben hat. Die Ausnahmen kann man über die Property Exception abrufen
  • IsCanceled: True wenn der Task abgebrochen wurde. Mehr dazu später
Warten auf Tasks

Bis jetzt können wir Tasks erstellen und den aktuellen Status abrufen. Theoretisch könnten wir selber eine Logik schreiben um auf den Task zu warten. Das ist aber nicht notwendig da es alles schon fix fertig gibt.

Im einfachsten Fall wenn man auf einen Task warten will sieht das ganze so aus:

Task t = new Task(MethodenName);
t.Start();
/*......*/
t.Wait();

Die Zeile t.Wait() blockiert den Thread in dem der Task erstellt und gestartet wurde so lange bis IsCompleted [true] ist. Das können einige Nanosektunden aber auch Stunden sein.

Hat man mehrere Tasks auf die man warten will kann man das elegant über die Statische Methode

Task t1 = new Task(MethodenName);
Task t2 = new Task(MethodenName2);
t1.Start();
t2.Start();
/*......*/
Task.WaitAll(t1, t2);

Ähnlich funktioniert auch die Statische Methode

Task.WaitAny(t1, t2);

Hier wird aber nur gewartet bis ein Task abgeschlossen ist. Danach läuft der Thread weiter. Der 2. Task wird aber trotzdem weiter ausgeführt.

Dieses Beispiel:

//Zur Abwechslung mal Actions statt Methoden 🙂
Action action = () =&gt;
{
    System.Threading.Thread.Sleep(1000);
    Console.WriteLine(&quot;Action Complete&quot;);
};
Action action2 = () =&gt;
{
    System.Threading.Thread.Sleep(3000);
    Console.WriteLine(&quot;Action2 Complete&quot;);
};

Task x1 = new Task(action);
Task x2 = new Task(action2);
x1.Start();
x2.Start();
/*......*/
Task.WaitAny(x1, x2);
Console.WriteLine(&quot;Wait Complete&quot;);

Liefert daher dieses Ergebnis

Action Complete
Wait Complete
Action2 Complete

Wie oben schon erwähnt kann es vorkommen das ein Task sehr lange dauern kann. In den meisten Fällen kann man die Zeit die eine Aufgabe braucht abschätzen und man will seinen Thread nicht ewig blockieren. Daher haben (fast) alle Wait Methoden zusätzliche Parameter um die Maximale Zeit die gewartet werden soll festzulegen.

t.Wait(1000);

gibt an das maximal 1000ms auf den Task gewartet werden soll. Ist der Task schneller fertig wird natürlich nicht mehr gewartet. Ist der Task aber nach 1000ms immer noch nicht abgeschlossen wird nicht mehr gewartet und der aktuelle Thread wird weiter ausgeführt. Der Task läuft aber trotzdem weiter im Hintergrund bis er abgeschlossen ist. Das ist einer der Gründe warum es eine Möglichkeit gibt Tasks “sauber” abzubrechen.

Fehlerbehandlung

Bevor es ans Abbrechen von Tasks geht muss noch kurz geklärt werden was passiert wenn ein Task eine nicht behandelten Ausnahme (Programmierer Jargon: Unhandled Exception) verursacht.

Das Thema ist recht komplex und könnte einen kompletten Artikel füllen ich werde mich aber hier auf das wesentliche konzentrieren. Fakt ist solange man nicht auf das Ergebnis eines Tasks zugreift ist der Thread der den Task gestartet hat nicht in Gefahr (ein paar Ausnahmen gibt es aber trotzdem). 

Über die Property IsFaulted kann man jederzeit abfragen ob ein Task “gecrashed” ist.

Ich habe ein kleines Consolen Programm geschrieben das dieses Verhalten schön aufzeigt:

static void Main(string[] args)
{

    var a = new Func&lt;double&gt;(() =&gt;
    {
        System.Threading.Thread.Sleep(3000);
        throw new Exception(&quot;Crash&quot;);
        return 42.1337;
    });

    Task&lt;double&gt; task = new Task&lt;double&gt;(a);
    task.Start();
    Console.WriteLine(&quot;1. Task started&quot;);
    Thread.Sleep(1000); //Simulation 1sec arbeit
    Console.WriteLine(&quot;2. Status:&quot; + task.Status + &quot; IsCompleted : &quot; + task.IsCompleted +
                        &quot; IsFaulted : &quot; + task.IsFaulted);
    Thread.Sleep(1000); //Simulation 1sec arbeit
    Console.WriteLine(&quot;3. Status:&quot; + task.Status + &quot; IsCompleted : &quot; + task.IsCompleted +
                        &quot; IsFaulted : &quot; + task.IsFaulted);
    Thread.Sleep(1000); //Simulation 1sec arbeit
    Console.WriteLine(&quot;4. Status:&quot; + task.Status + &quot; IsCompleted : &quot; + task.IsCompleted +
                        &quot; IsFaulted : &quot; + task.IsFaulted);
    Thread.Sleep(1000); //Simulation 1sec arbeit
    Console.WriteLine(&quot;5. Status:&quot; + task.Status + &quot; IsCompleted : &quot; + task.IsCompleted +
                    &quot; IsFaulted : &quot; + task.IsFaulted);
    Console.WriteLine(&quot;6. Main work completed&quot;);
    Console.ReadKey();
}

Der Output dazu sieht so aus:

1. Task started
2. Status:Running IsCompleted : False IsFaulted : False
3. Status:Running IsCompleted : False IsFaulted : False
4. Status:Running IsCompleted : False IsFaulted : False
5. Status:Faulted IsCompleted : True IsFaulted : True
6. Main work completed

Man sieht schön das der Ausführende Thread einfach weiter lief obwohl der Task schon gecrashed war. Ein Problem hat man erst wenn man auf das Ergebnis zugreifen will.

Im follgenden Beispiel möchte ich auf das Ergebnis des Tasks warten, ich verwende task.Result das implizit ein Wait() ausführt:

static void Main(string[] args)
{

    var a = new Func&lt;double&gt;(() =&gt;
    {
        System.Threading.Thread.Sleep(3000);
        throw new Exception(&quot;Crash&quot;);
        return 42.1337;
    });

    Task&lt;double&gt; task = new Task&lt;double&gt;(a);
    task.Start();
    Console.WriteLine(&quot;1. Task started&quot;);
    Thread.Sleep(1000); //Simulation 1sec arbeit
    double result = task.Result;
    Thread.Sleep(1000); //Simulation 1sec arbeit
    Console.WriteLine(&quot;6. Main work completed&quot;);
    Console.ReadKey();
}

Das Resultat dieses Umbaus sieht dann so aus:

1. Task started

Unhandled Exception: System.AggregateException: One or more errors occurred. ---
&gt; System.Exception: Crash
   at Tasks1.Program.&lt;&gt;c.&lt;Main&gt;b__0_0() in Program.cs:line 22
   at System.Threading.Tasks.Task`1.InnerInvoke()
   at System.Threading.Tasks.Task.Execute()
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceled
Exceptions)
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotifica
tion)
   at System.Threading.Tasks.Task`1.get_Result()
   at Tasks1.Program.Main(String[] args) in Program.cs:line 30

Der Zugriff auf task.Result bringt die komplette Applikation zum Absturz. Es ist sehr empfehlenswert jedes Wait also auch das task.Result in einen try/catch Block zu verpacken. Die Exception ist in diesem Fall immer  vom Typ System.AggregateException den genauen Grund findet man in den InnerExceptions Property.

Hier das letzte Beispiel mit einem einfachen Exception Handling:

static void Main(string[] args)
{

    var a = new Func&lt;double&gt;(() =&gt;
    {
        System.Threading.Thread.Sleep(3000);
        throw new Exception(&quot;Crash&quot;);
        return 42.1337;
    });

    Task&lt;double&gt; task = new Task&lt;double&gt;(a);
    task.Start();
    Console.WriteLine(&quot;1. Task started&quot;);
    Thread.Sleep(1000); //Simulation 1sec arbeit
    double result;
    try
    {
        result = task.Result;
    }
    catch (AggregateException ae)
    {
        Console.WriteLine(&quot;AggregateException occurred&quot;);
        foreach (var innerException in ae.InnerExceptions)
        {
            Console.WriteLine(&quot;   Inner:&quot; + innerException.Message);
        }
    }

    Thread.Sleep(1000); //Simulation 1sec arbeit
    Console.WriteLine(&quot;6. Main work completed&quot;);
    Console.ReadKey();
}

Das Ergebnis dieses mal:

1. Task started
AggregateException occurred
   Inner:Crash
6. Main work completed

In diesem Beispiel sieht das sehr einfach und logisch aus wenn das ganze dann aber etwas komplexer und verschachtelt wird vergisst man schnell mal auf solche “Kleinigkeiten”.

Abbrechen von Tasks

Wie schon gesagt läuft ein Task bis er abgeschlossen sind. Das kann auf 3 Arten passieren. Er ist mit der “Arbeit” fertig. Er produziert einen unbehandelte Ausnahme Fehler (Unhandled Exception). Oder er wird Abgebrochen. Um den letzten Fall geht es in diesem Abschnitt.

Ich beginne mal mit der Wichtigsten Information: Man kann einen Task nicht jederzeit abbrechen. Daher muss man seinen Task genau planen. Um einen Task überhaupt abbrechen zu können muss man ihm ein CancellationToken mitgeben. Am Besten kann man das ganze mit einem Beispiel erklären.

Im folgendem Beispiel Soll der Task so lange etwas “arbeiten” bis wir in unserem Hauptthread eine andere Aufgabe erledigt haben. Der Einfachheit halber verwende ich wieder Thread.Sleep für die Simulation der “Arbeit” dieses mal aber verpackt in eine Methode das es etwas übersichtlicher wird

Hier das Beispiel:

static void Main(string[] args)
        {

            var cancelSource = new CancellationTokenSource();
            var token = cancelSource.Token;

            var a = new Action(() =&gt;
            {
                int i = 0;
                while (true)
                {
                    System.Threading.Thread.Sleep(333);
                    Console.WriteLine(&quot;Task is working on part :&quot; + i++);

                    // Den Task abbrechen wenn das Token ausgel&#246;st wurde
                    token.ThrowIfCancellationRequested();
                }
            });

            Task task = new Task(a, token);
            task.Start();


            DoWorkAndDisplayTaskStatus(task);
            DoWorkAndDisplayTaskStatus(task);
            DoWorkAndDisplayTaskStatus(task);


            Console.WriteLine(&quot;Request Cancel&quot;);
            cancelSource.Cancel();
            try
            {
                task.Wait();
            }
            catch (AggregateException ae)
            {
                if (ae.InnerException is OperationCanceledException)
                    Console.WriteLine(&quot;Task is canceled !&quot;);
            }

            DoWorkAndDisplayTaskStatus(task);
            DoWorkAndDisplayTaskStatus(task);
            DoWorkAndDisplayTaskStatus(task);

            Console.WriteLine(&quot;END&quot;);
            Console.ReadKey();

        }

        private static void DoWorkAndDisplayTaskStatus(Task task)
        {
            Thread.Sleep(1000); //Simulation 1sec arbeit
            Console.WriteLine(&quot;Status:&quot; + task.Status + &quot; IsCompleted : &quot; + task.IsCompleted +
                                &quot; IsFaulted : &quot; + task.IsFaulted + &quot; IsCanceled : &quot; + task.IsCanceled);
        }

 

Gleich in den Ersten Zeilen befindet sich der Code um ein CancellationToken zu erzeugen

var cancelSource = new CancellationTokenSource();
var token = cancelSource.Token;

Das Token wird dann bei der Instanziierung des Tasks mitgegeben

Task task = new Task(a,token);

Nach etwas “Arbeit” wird das Token ausgelöst

cancelSource.Cancel();

Bis jetzt klingt alles mal logisch und verständlich. Aber wenn man sich den Task Code ansieht findet man hier eine recht untypische Zeile Code:

token.ThrowIfCancellationRequested();

Was sie tut kann man am Namen schon erahnen es wird eine Ausnahme ausgelöst wenn das Token ausgelöst wurde. Genauer gesagt wird eine Ausnahme vom Typ OperationCanceledException ausgelöst. Diese Ausnahme wird dann beim task.Wait() abgefangen

 try
    {
        task.Wait();
    }
    catch (AggregateException ae)
    {
        if (ae.InnerException is OperationCanceledException)
            Console.WriteLine(&quot;Task is canceled !&quot;);                               
    }

Einige werden sich fragen warum das so kompliziert gemacht wird es würde ja ein Boolean ausreichen und dann macht man einfach ein return um den Task zu verlassen.

Das klingt einleuchtend und leider gibt es diesen Boolean sogar (token.IsCancellationRequested). Die Betonung liegt hier auf leider da oft der Fehler gemacht wird diesen Boolean genau so zu verwenden. Verwendet man IsCancellationRequested und macht dann ein return wird der Task beendet IsCompleted ist auf true ABER IsCanceled bleibt auf false und der Status des Tasks ist “RunToCompletion”.

Wenn man mit mehreren Tasks arbeitet diese dann auch noch verkettet, Child Tasks erstellt usw…  dann spielt das eine sehr große Rolle. Auf diese Sachen werde ich bei Interesse in einem weiteren Tutorial eingehen.

Der Vollständigkeit halber poste ich noch das Beispiel mit dem IsCancellationRequested Flag aber meine Empfehlung ist es nur zu verwenden wenn man genau weis wie dieser Task verwendet wird.

static void Main(string[] args)
{

    var cancelSource = new CancellationTokenSource();
    var token = cancelSource.Token;

    var a = new Action(() =&gt;
    {
        int i = 0;
        // Eine Schleife die so lange ausgef&#252;hrt wird bis der Task abgebrochen wird
        while (!token.IsCancellationRequested) 
        {
            System.Threading.Thread.Sleep(333);
            Console.WriteLine(&quot;Task is working on part :&quot; + i++);
        }
    });

    Task task = new Task(a, token);
    task.Start();


    DoWorkAndDisplayTaskStatus(task);
    DoWorkAndDisplayTaskStatus(task);
    DoWorkAndDisplayTaskStatus(task);


    Console.WriteLine(&quot;Request Cancel&quot;);
    cancelSource.Cancel();
    try
    {
        task.Wait();
    }
    catch (AggregateException ae) //Sollte in diesem Beispiel nicht passieren 🙂
    {
        Console.WriteLine(&quot;AggregateException occurred&quot;);
        foreach (var innerException in ae.InnerExceptions)
        {
            Console.WriteLine(&quot;   Inner:&quot; + innerException.Message);
        }
    }

    DoWorkAndDisplayTaskStatus(task);
    DoWorkAndDisplayTaskStatus(task);
    DoWorkAndDisplayTaskStatus(task);

    Console.WriteLine(&quot;END&quot;);
    Console.ReadKey();

}
Zusammenfassung

Mit dieser kleinen Anleitung sollte es jetzt möglich sein einen Task zu starten, auf ein Ergebnis zu warten und einen Task abzubrechen. Die Task Parallel Library bietet noch sehr viel mehr aber wie bei allen anderen Dingen heißt es mal klein anfangen um später großes zu bewirken 🙂

Aufbauend auf dieses Tutorial wird es bald einen zum Thema async und await geben das wie wir dann lernen werden sehr eng mit den Tasks verwand ist.