Multithreading in C#, Teil 23: Task Continuations

Tasks können in der Ausführungsreihenfolge voneinander abhängen. Ein typischer Anwendungsfall besteht darin, einen Task zu starten nachdem eine Vorgängertask beendet wurde. In diesem Artikel möchte ich Ihnen zeigen wie sie diese Funktionalität mit Hilfe der Task Klasse implementieren können.

 
Task mit Folgetask

Im nachfolgenden Beispiel werden zwei Tasks erstellt. Der zweite Task soll dabei als Nachfolger des ersten Tasks ausgeführt werden. Dazu wird er mittels der ContinueWith Funktion erzeugt. Dabei wird dem Folgetask als Parameter die Instanz des Vorgängertasks mitgegeben.

class Program
{
    static void Main(string[] args)
    {
        Task task1 = new Task(() => Console.WriteLine("Task 1"));

        Task task2 = task1.ContinueWith(task => Console.WriteLine("Task 2"));

        task1.Start();

        Console.Read();
    }
}

 
Im vorhergehenden Beispiel wurden beide Tasks erst angelegt und danach explizit gestartet. Es ist aber auch möglich einen direkten Start beim Erstellen der Tasks durchzuführen. Der nachfolgende Quellcode zeigt die angepasste Beispielanwendung. Sollte der erste Task bereits vor dem Erzeugen des zweiten Tasks beendet werden, dann wird der zweite Task direkt gestartet.

class Program
{
    static void Main(string[] args)
    {
        Task task1 = Task.Factory.StartNew(() => Console.WriteLine("Task 1"));

        Task task2 = task1.ContinueWith(task => Console.WriteLine("Task 2"));

        Console.Read();
    }
}

 
Verkettete Tasks

Mittels der ContinueWith Funktion lassen sich mehrere Tasks miteinander verketten. Da diese Funktion jeweils eine Task Instanz zurückgibt kann diese Instanz gleich zum nächsten Aufruf von ContinueWith genutzt werden. Das nachfolgende Beispiel zeigt wie vier verkettete Tasks erstellt werden können.

class Program
{
    static void Main(string[] args)
    {
        Task.Factory.StartNew(() => Console.WriteLine("Task 1"))
            .ContinueWith(task => Console.WriteLine("Task 2"))
            .ContinueWith(task => Console.WriteLine("Task 3"))
            .ContinueWith(task => Console.WriteLine("Task 4"));

        Console.Read();
    }
}

 
Task mit Rückgabewert

Der verkettet aufgerufene Task kann einen Rückgabewert haben. Der nachfolgende Quellcode zeigt eine Funktion mit Rückgabewert, welche verkettet aufgerufen wird. Der Rückgabewert wird dabei, wie bei Tasks gewohnt, mittels des Result Properties ausgelesen.

class Program
{
    static void Main(string[] args)
    {
        Task task1 = new Task(() => Console.WriteLine("Task 1"));

        Task<string> task2 = task1.ContinueWith<string>(_ => DoWork());

        task1.Start();

        Console.WriteLine(task2.Result);
        Console.Read();
    }

    static string DoWork()
    {
        Console.WriteLine("Task 2");
        return "result of task";
    }
}

 
Task mit mehreren Vorgängern

In den bisher gezeigten Beispielen wurde ein Task aufbauend auf einem anderen Task gestartet. Neben diesen einfachen Verkettungen sind auch komplexere Abhängigkeiten realisierbar. So kann ein Task beispielweise mehrere Vorgänger haben. Der Tasks wird entweder gestartet wenn alle Vorgänger beendet wurden oder wenn einer der Vorgänger beendet wurde. Diese Abhängigkeit lässt sich mittels der Funktionen ContinueWhenAll beziehungsweise ContinueWhenAny der Klasse Task.Factory erzeugen. Das nachfolgende Beispiel zeigt drei Tasks, wobei der dritte Task gestartet wird wenn die beiden Vorgängertasks beendet wurden. Die tasks Variable dient dabei zur Übergabe eines Arrays mit den beendeten Tasks.

class Program
{
    static void Main(string[] args)
    {
        Task task1 = Task.Factory.StartNew(() => Console.WriteLine("Task 1"));
        Task task2 = Task.Factory.StartNew(() => Console.WriteLine("Task 2"));

        Task task3 = Task.Factory.ContinueWhenAll(
            new[] { task1, task2 },
            tasks => Console.WriteLine("Task 3"));

        Console.Read();
    }
}

 
Task mit mehreren Nachfolgern

Wenn ein Task mehrere Vorgänger haben kann, liegt die Vermutung nahe, dass ein Task auch mehrere Nachfolger haben kann. Diese Verkettung ist durch den mehrfachen Aufruf der ContinueWith Funktion realisierbar. Das nachfolgende Beispiel zeigt einen Task welcher zwei Folgetasks hat.

class Program
{
    static void Main(string[] args)
    {
        Task task1 = new Task(() => Console.WriteLine("Task 1"));

        Task task2 = task1.ContinueWith(task => Console.WriteLine("Task 2"));
        Task task3 = task1.ContinueWith(task => Console.WriteLine("Task 3"));

        task1.Start();

        Console.Read();
    }
}

 
Fehlerbehandlung

Ein Task kann feststellen ob ein Fehler von seinem Vorgängertask geworfen wurde. Dazu kann das Exception Property des entsprechenden Tasks abgefragt werden. Der nachfolgende Quellcode zeigt wie im zweiten Task der Fehler des ersten Tasks abgefragt und ausgegeben wird.

class Program
{
    static void Main(string[] args)
    {
        Task task1 = Task.Factory.StartNew(DoWork1);

        Task task2 = task1.ContinueWith(task => DoWork2(task));

        Console.Read();
    }

    static void DoWork1()
    {
        Console.WriteLine("Task 1");

        int i = 0;
        i = i / i;
    }

    static void DoWork2(Task antecedentTask)
    {
        Console.WriteLine("Task 2");

        if (antecedentTask.Exception != null)
        {
            Console.WriteLine(antecedentTask.Exception.InnerException.Message);
        }
    }
}

 
Child Tasks

Erzeugt ein Task weitere Child Tasks, dann wird dieser erst als beendet angesehen wenn auch alle Child Tasks beendet wurden. Das heisst, ein Folgetasks wird erst dann gestartet, wenn der Vorgängertask inklusive all seiner Child Tasks beendet wurde.

class Program
{
    static void Main(string[] args)
    {
        Task task1 = new Task(() =>
            {
                Console.WriteLine("Task 1");
                Task.Factory.StartNew(() => Console.WriteLine("Child 1.1"), TaskCreationOptions.AttachedToParent);
                Task.Factory.StartNew(() => Console.WriteLine("Child 1.2"), TaskCreationOptions.AttachedToParent);
            });

        Task task2 = task1.ContinueWith(task => Console.WriteLine("Task 2"));

        task1.Start();

        Console.Read();
    }
}

 
Bedingte Nachfolgetasks

Es ist möglich einen oder mehrere Nachfolgetasks anzugeben welche nur unter bestimmten Bedingungen ausgeführt werden. Dabei können Sie als Bedingung den Abschlusszustand des Vorgängertasks mittels TaskContinuationOptions angeben.

Die folgenden Bedingungen stehen zur Verfügung:

NotOnRanToCompletion Nachfolgetasks wird nicht ausgeführt wenn   Vorgängertask normal beendet wurde.
NotOnFaulted Nachfolgetasks wird nicht ausgeführt wenn Vorgängertask   fehlerhaft beendet wurde.
NotOnCanceled Nachfolgetasks wird nicht ausgeführt wenn   Vorgängertask frühzeitig abgebrochen wurde.

 
Diese Negativbedingungen wiedersprechen der allgemeinen Programmierrichtlinie nur Positivformen zu verwenden. Es stehen aber drei weitere Werte zur Auswahl, welche eine Kombination der obigen Flags darstellen und leichter zu verwenden sind:

OnlyOnRanToCompletion Nachfolgetasks wird ausgeführt wenn Vorgängertask   normal beendet wurde.
OnlyOnFaulted Nachfolgetasks wird ausgeführt wenn Vorgängertask   fehlerhaft beendet wurde.
OnlyOnCanceled Nachfolgetasks wird ausgeführt wenn Vorgängertask   frühzeitig abgebrochen wurde.

 
Fazit

Mittels der Taks Continuations ist es relativ leicht möglich auch komplexere Szenarien von Task-Abfolgen abzubilden. Dabei können einfache 1-1 Beziehungen bis hin zu m-n Beziehungen zwischen Vorgänger- und Nachfolgertasks erstellt werden. Selbst bedingte Abfolgen lassen sich mittels optionalen Parametern angeben. Zu guter Letzt wird auch die Fehlerbehandlung vereinfacht und lässt eine Weitergabe von Fehlern zwischen Tasks zu.

Advertisements
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 )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s