System.Threading.Tasks Basics Teil 2

Im Ersten Teil der Tutorial Reihe über Tasks habe ich gezeigt wie man Tasks erstellt, Startet, Abbricht und was im Fehlerfall passiert. Im Teil 2 geht es um die Weiterführung von Tasks. Wir werden also Tasks hintereinander Starten.

Task.ContinueWith

Im einfachsten Fall sieht das dann so aus:

Task task1 = Task.Factory.StartNew(() => Console.Write("Task 1..."));
Task task2 = task1.ContinueWith(t1=> Console.Write("Task 2"));

Der Erste Task wird über die Task Factory erstellt und gleich gestartet. Der task2 wird an den ersten angehängt und gestartet sobald der task1 Completed ist.

man kann die Kette beliebig lange fortsetzen…

Hier ein paar Wichtige Informationen dazu:

  1. ContinueWith  hat immer einen InputParameter und dieser ist der Task der zuvor ausgeführt wurde. Daher kann man auch auf das Ergebnis des letzten Tasks zugreifen.
  2. Wie schon im Teil 1 des Tutorials aufgezeigt Der Task ist auch dann Completed wenn er Abgebrochen oder Fehlgeschlagen ist.
  3. Wenn der Task schon abgeschlossen ist kann man trotzdem ein ContinueWith machen. Der Folgetask wird dadurch sofort Ausgeführt.
  4. Ohne zusätzliche Angabe ist nicht sichergestellt das alle Tasks im gleichen Thread laufen. Bei sehr kurzen Threads kann das zu einem Performance Problem führen. Umgehen kann man das wenn man beim ContinueWith TaskContinuationOptions.ExecuteSynchronously angibt.
Ergebnisse und Fehler weiter geben

Wie zuvor kurz angekündigt wird dem 2. Task immer der Erste Task als Parameter mitübergeben. Da der Erste Task immer Fertig ist gibt es auch ein Result. Zumindest dann wenn es ein Ergebnis gibt 🙂

Hier ein kleines Beispiel

static void Main(string[] args)
{
    try
    {


        var data = new List<double> { 1.35, 13.76 };
        var t1 = new Task<double>(input =>
        {
            var i = input as List<double>;
            //throw new Exception("Ich bin ein böser Fehler :)");
            return i.Average();

        }, data);
        var t2 = t1.ContinueWith(
            parentTask =>
            {
                var calc = parentTask.Result * Math.PI;
                Console.WriteLine("Task 2 hat dieses Ergebnis von task 1 erhalten: "
            + parentTask.Result);
                return calc;
            });
        var t3 = t2.ContinueWith(parentTask =>
        {
            Console.WriteLine("Task 3 hat dieses Ergebnis von task 2 erhalten: "
                + parentTask.Result);

        });
        t1.Start();
        t3.Wait();
    }
    catch (AggregateException exception)
    {
        Console.WriteLine("EX:" + exception.Message);
        foreach (var innerException in exception.InnerExceptions)
            Console.WriteLine("INNER EX:" + innerException.Message);



    }
    Console.WriteLine("Press any key to exit");
    Console.ReadKey();
}

3 Tasks die hinter einander ausgeführt werden sollen.  So wie das Beispiel im Moment geschrieben ist gibt es aber keinen Fehler und man bekommt diesen Wunderbaren Output:

Task 2 hat dieses Ergebnis von task 1 erhalten: 7,555
Task 3 hat dieses Ergebnis von task 2 erhalten: 23,7347324978709
Press any key to exit

Wenn man jetzt im task1 das Kommentar von dieser Zeile entfernt

//throw new Exception("Ich bin ein böser Fehler :)");

wird der task1 mit einer Exception beendet. In der Praxis kann das z.b. eine NullrefenceException, IOException oder vergleichbares sein. In meinem Fall eine Exception mit der Message “Ich bin ein böser Fehler :)”

Am Besten man startet jetzt das Programm ohne Debugger (Strg+F5)

Der Output sieht dann so aus:

EX:One or more errors occurred.
INNER EX:One or more errors occurred.
Press any key to exit

So wo ist jetzt die Exception mit der Message “Ich bin ein böser Fehler :)” ?

Nochmal zur Erinnerung Jeder Task wirft AggregateExceptions und in der InnerException findet man dann den eigentlichen Fehler. UND task mit ContinueWith werden auch aufgerufen wenn der vorherige Task einen Fehlgeschlagen ist.

task1 wirft also eine AggregateExceptions  und hat als InnerException den bösen Fehler. Da durch die Exception der Task dadurch auf IsCompleted gesetzt wird startet sofort task2 der beim Zugriff auf das Ergebnis von task1 eine Exception vom Typ AggregateException wirft die InnerException ist die Exception aus task1 die wiederum hat als InnerException den bösen Fehler. Gleiches gilt dann nochmal für task3 also findet man im catch dann die Exception mit dem bösen Fehler Text genau hier:

exception.InnerException.InnerException.InnerException

Verstanden? Hoffentlich denn bevor ich die Lösung dieses Problems präsentiere wird jetzt noch ein bisschen komplizierter.

Ich Ändere den Code im task2 so ab das er nichtmehr vom task1 abhängig ist. In der Praxis könnte das etwa so sein: task1 lädt ein File von einem Server und speichert das immer unter den Namen abc.xml auf die Platte. im task2 wird das File von der Platte gelesen und ausgewertet. task3 schreibt dann das Ergebnis. Ok ist kein sehr Praxis Nahes Beispiel aber ich kenne Programmierer die machen so etwas 🙂

Das neue Beispiel sieht dann so aus:

static void Main(string[] args)
{
    try
    {


        var data = new List<double> { 1.35, 13.76 };
        var t1 = new Task<double>(input =>
        {
            var i = input as List<double>;
            throw new Exception("Ich bin ein böser Fehler :)");
            return i.Average();

        }, data);
        var t2 = t1.ContinueWith(
            parentTask =>
            {
                Console.WriteLine("Do nothing with task1");
                return 123.42;
            });
        var t3 = t2.ContinueWith(parentTask =>
        {
            Console.WriteLine("Task 3 hat dieses Ergebnis von task 2 erhalten: "
                + parentTask.Result);

        });
        t1.Start();
        t3.Wait();
    }
    catch (AggregateException exception)
    {
        Console.WriteLine("EX:" + exception.Message);
        foreach (var innerException in exception.InnerExceptions)
            Console.WriteLine("INNER EX:" + innerException.Message);



    }
    Console.WriteLine("Press any key to exit");
    Console.ReadKey();
}

Das Beispiel am besten wieder ohne Debugger (Strg+F5) starten.  Und sich den schönen Output ansehen:

Do nothing with task1
Task 3 hat dieses Ergebnis von task 2 erhalten: 123,42
Press any key to exit

Juhu keine Exception mehr!!! Aber richtig funktionieren tut das Programm auch nicht. Und das schlimmste daran ist das es keiner merkt.

Was ist passiert?

task1 hatte den üblichen Fehler. task2 wurde gestartet und hat seine Berechnung erfolgreich abgeschlossen. task3 wurde gestartet und läuft auch sauber durch da task2 ja ein gültiges Ergebnis liefert.

In einem 20 Zeilen Consolen Program fällt einem dieser Fehler vielleicht noch auf in einer Applikation mit tausenden Zeilen und mehr eher nicht.

Was also tun? man könnte einfach in jedem abhängigen Task mal prüfen ob der vorherige Task einen Fehler hatte. Und wenn ja diesen Fehler dann weiter an den nächsten Task geben. Man angenommen man hat 100 aufeinander folgende Tasks dann muss man also 100x den Fehler weiter geben und dann könnte man den Ursprungs Fehler in der letzten der bis zu 100x verschachtelten  InnerException,…. finden.

Oder man programmiert sauber und verhindert das die Folge Tasks erst gar nicht ausgeführt werden. Das geht ganz einfach in dem man beim task2 angibt das er nur ausgeführt werden soll wenn der task1 nicht fehlschlägt. Das geht ganz einfach über den Parameter taskContinuationOptions in userem Fall wollen wir da TaskContinuationOptions.NotOnFaulted übergeben.