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.