Fortsetzungsaufgaben
Bei der asynchronen Programmierung werden nach Abschluss eines asynchronen Vorgangs häufig ein zweiter Vorgang aufgerufen und Daten an diesen weitergegeben. In der Vergangenheit wurden hierfür vor allem Rückrufmethoden genutzt. In der Task Parallel Library wird die gleiche Funktionalität durch Fortsetzungsaufgaben bereitgestellt. Eine Fortsetzungsaufgabe (auch kurz als Fortsetzung bezeichnet) ist eine asynchrone Aufgabe, die von einer anderen Aufgabe, die wiederum als Vorgänger bezeichnet wird, nach deren Abschluss aufgerufen wird.
Fortsetzungen können relativ einfach, gleichzeitig jedoch äußerst leistungsstark und flexibel eingesetzt werden. Sie haben unter anderem folgende Möglichkeiten:
Übergeben von Daten vom Vorgänger an die Fortsetzung
Angeben der präzisen Bedingungen, unter denen die Fortsetzung aufgerufen bzw. nicht aufgerufen wird
Abbrechen einer Fortsetzung, bevor diese gestartet wird oder während sie ausgeführt wird
Bereitstellen von Hinweisen zur Planung der Fortsetzung
Aufrufen mehrerer Fortsetzungen durch den gleichen Vorgänger
Aufrufen einer Fortsetzung auf, wenn alle Vorgänger oder einer der Vorgänger abgeschlossen wird
Verketten von Fortsetzungen auf eine beliebige Länge
Beheben von durch den Vorgänger ausgelöste Ausnahmen mithilfe einer Fortsetzung
Erstellen Sie Fortsetzungen mit der Task.ContinueWith-Methode. Im folgenden Beispiel ist das grundlegende Muster dargestellt (aus Gründen der Übersichtlichkeit wird die Ausnahmebehandlung ausgelassen).
' The antecedent task. Can also be created with Task.Factory.StartNew.
Dim taskA As Task(Of DayOfWeek) = New Task(Of DayOfWeek)(Function()
Return DateTime.Today.DayOfWeek
End Function)
' The continuation. Its delegate takes the antecedent task
' as an argument and can return a different type.
Dim continuation As Task(Of String) = taskA.ContinueWith(Function(antecedent)
Return String.Format("Today is {0}", antecedent.Result)
End Function)
' Start the antecedent.
taskA.Start()
' Use the contuation's result.
Console.WriteLine(continuation.Result)
// The antecedent task. Can also be created with Task.Factory.StartNew.
Task<DayOfWeek> taskA = new Task<DayOfWeek>(() => DateTime.Today.DayOfWeek);
// The continuation. Its delegate takes the antecedent task
// as an argument and can return a different type.
Task<string> continuation = taskA.ContinueWith((antecedent) =>
{
return String.Format("Today is {0}.",
antecedent.Result);
});
// Start the antecedent.
taskA.Start();
// Use the contuation's result.
Console.WriteLine(continuation.Result);
Sie können auch eine Fortsetzung mit mehreren Aufgaben erstellen, die beim Abschluss von einer oder allen Aufgaben in einem Array ausgeführt wird, wie im folgenden Beispiel gezeigt.
Dim task1 As Task(Of Integer) = New Task(Of Integer)(Function()
' Do some work...
Return 34
End Function)
Dim task2 As Task(Of Integer) = New Task(Of Integer)(Function()
' Do some work...
Return 8
End Function)
Dim tasks() As Task(Of Integer) = {task1, task2}
Dim continuation = Task.Factory.ContinueWhenAll(tasks, Sub(antecedents)
Dim answer As Integer = tasks(0).Result + tasks(1).Result
Console.WriteLine("The answer is {0}", answer)
End Sub)
task1.Start()
task2.Start()
continuation.Wait()
Task<int>[] tasks = new Task<int>[2];
tasks[0] = new Task<int>(() =>
{
// Do some work...
return 34;
});
tasks[1] = new Task<int>(() =>
{
// Do some work...
return 8;
});
var continuation = Task.Factory.ContinueWhenAll(
tasks,
(antecedents) =>
{
int answer = tasks[0].Result + tasks[1].Result;
Console.WriteLine("The answer is {0}", answer);
});
tasks[0].Start();
tasks[1].Start();
continuation.Wait();
Eine Fortsetzung wird im WaitingForActivation-Zustand erstellt und kann daher nur von der zugehörigen Vorgängeraufgabe gestartet werden. Beim Aufruf von Task.Start für eine Fortsetzung im Benutzercode wird eine System.InvalidOperationException ausgelöst.
Eine Fortsetzung ist selbst ein Task und blockiert nicht den Thread, in dem sie gestartet wird. Verwenden Sie die Wait-Methode, um den Thread bis zum Abschluss der Fortsetzungsaufgabe zu blockieren.
Fortsetzungsoptionen
Wenn Sie eine Fortsetzung mit einer einzelnen Aufgabe erstellen, können Sie eine ContinueWith-Überladung mit der System.Threading.Tasks.TaskContinuationOptions-Enumeration verwenden, um die Bedingungen anzugeben, unter denen die Vorgängeraufgabe die Fortsetzung startet. Sie können beispielsweise angeben, dass die Fortsetzung nur ausgeführt werden soll, wenn der Vorgänger abgeschlossen wurde, wenn der Vorgänger mit einem Fehler abgeschlossen wurde usw. Wenn die Bedingung zu dem Zeitpunkt, zu dem der Vorgänger die Fortsetzung aufrufen würde, nicht erfüllt ist, geht die Fortsetzung direkt in den Zustand Canceled über und kann anschließend nicht mehr gestartet werden. Wenn Sie für eine Fortsetzung mit mehreren Aufgaben die NotOn-Option oder die OnlyOn-Option angeben, wird zur Laufzeit eine Ausnahme ausgelöst.
Die System.Threading.Tasks.TaskContinuationOptions-Enumeration enthält die gleichen Optionen wie die System.Threading.Tasks.TaskCreationOptions-Enumeration. AttachedToParent, LongRunning und PreferFairness haben in beiden Enumerationstypen die gleiche Bedeutung und den gleichen Wert. Diese Optionen können für Fortsetzungen mit mehreren Aufgaben verwendet werden.
In der folgenden Tabelle werden alle Werte von TaskContinuationOptions aufgeführt.
Element |
Beschreibung |
---|---|
Gibt das Standardverhalten an, wann keine TaskContinuationOptions angegeben sind. Die Fortsetzung wird geplant, wenn der Vorgänger abgeschlossen wird, unabhängig vom Endstatus des Vorgängers. Handelt es sich bei der Aufgabe um eine untergeordnete Aufgabe, wird diese als getrennte geschachtelte Aufgabe erstellt. |
|
Plant die Fortsetzung so, dass früher geplante Aufgaben mit großer Wahrscheinlichkeit früher ausgeführt werden als Aufgaben, die später geplant wurden. |
|
Gibt an, dass die Fortsetzung ein undifferenzierter Vorgang mit langer Laufzeit ist. Enthält einen Hinweis für das System.Threading.Tasks.TaskScheduler, dass möglicherweise zu viele Abonnements gewährt werden. |
|
Gibt an, dass die Fortsetzung, sofern es sich um eine untergeordnete Aufgabe handelt, in der Aufgabenhierarchie mit einem übergeordneten Element verknüpft wird. Die Fortsetzung ist nur dann eine untergeordnete Aufgabe, wenn der zugehörige Vorgänger auch eine untergeordnete Aufgabe ist. |
|
Gibt an, dass die Fortsetzung nicht geplant werden soll, wenn der Vorgänger bis zum Abschluss ausgeführt wurde. |
|
Gibt an, dass die Fortsetzung nicht geplant werden soll, wenn der Vorgänger einen Ausnahmefehler ausgelöst hat. |
|
Gibt an, dass die Fortsetzung nicht geplant werden soll, wenn der Vorgänger abgebrochen wurde. |
|
Gibt an, dass die Fortsetzung nur geplant werden soll, wenn der Vorgänger bis zum Abschluss ausgeführt wurde. |
|
Gibt an, dass die Fortsetzung nur geplant werden soll, wenn der Vorgänger einen Ausnahmefehler ausgelöst hat. Bei Verwendung der OnlyOnFaulted-Option ist sichergestellt, dass die Exception-Eigenschaft im Vorgänger nicht NULL ist. Sie können diese Eigenschaft verwenden, um die Ausnahme zu erfassen und anzuzeigen, welche Ausnahme das Fehlschlagen der Aufgabe verursacht hat. Wenn Sie nicht auf die Exception-Eigenschaft zugreifen, bleibt die Ausnahme unbehandelt. Darüber hinaus wird eine neue Ausnahme ausgelöst, wenn Sie auf die Result-Eigenschaft einer Aufgabe zugreifen, die abgebrochen wurde oder fehlgeschlagen ist. |
|
Gibt an, dass die Fortsetzung nur geplant werden soll, wenn der Vorgänger mit dem Zustand Canceled abgeschlossen wurde. |
|
Für Fortsetzungen mit sehr kurzer Laufzeit. Gibt an, dass die Fortsetzung im Idealfall in dem gleichen Thread ausgeführt werden soll, der den Wechsel des Vorgängers in den Endzustand ausgelöst hat. Falls der Vorgänger beim Erstellen der Fortsetzung bereits abgeschlossen wurde, versucht das System, die Fortsetzung in dem Thread auszuführen, der die Fortsetzung erstellt hat. Wenn die CancellationTokenSource des Vorgängers in einem finally-Block (Finally in Visual Basic) verworfen wird, wird eine Fortsetzung mit dieser Option in diesem finally-Block ausgeführt. |
Übergeben von Daten an eine Fortsetzung
Ein Verweis auf den Vorgänger wird als Argument an den Benutzerdelegaten der Fortsetzung übergeben. Wenn der Vorgänger eine System.Threading.Tasks.Task<TResult> ist und die Aufgabe abgeschlossen wurde, kann die Fortsetzung auf die Task<TResult>.Result-Eigenschaft der Aufgabe zugreifen. Bei einer Fortsetzung mit mehreren Aufgaben und der Task.WaitAll-Methode ist das Argument ein Array aus Vorgängern. Bei Verwendung von Task.WaitAny ist das Argument der erste Vorgänger, der abgeschlossen wurde.
Task<TResult>.Result ist blockiert, bis die Aufgabe abgeschlossen wurde. Wenn die Aufgabe abgebrochen wurde oder fehlgeschlagen ist, löst Result jedoch beim Zugriff hierauf eine Ausnahme aus. Sie können dieses Problem vermeiden, indem Sie die OnlyOnRanToCompletion-Option wie im folgenden Beispiel gezeigt angeben.
Dim aTask = Task(Of Integer).Factory.StartNew(Function()
Return 54
End Function)
Dim bTask = aTask.ContinueWith(Sub(antecedent)
Console.WriteLine("continuation {0}", antecedent.Result)
End Sub,
TaskContinuationOptions.OnlyOnRanToCompletion)
var t = Task<int>.Factory.StartNew(() => 54);
var c = t.ContinueWith((antecedent) =>
{
Console.WriteLine("continuation {0}", antecedent.Result);
},
TaskContinuationOptions.OnlyOnRanToCompletion);
Um die Fortsetzung auch dann auszuführen, wenn der Vorgänger nicht abgeschlossen wurde, müssen Sie sich vor dieser Ausnahme schützen. Ein möglicher Ansatz besteht darin, den Status des Vorgängers zu überprüfen und nur dann auf Result zuzugreifen, wenn der Status nicht Faulted oder Canceled lautet. Sie können jedoch auch die Exception-Eigenschaft des Vorgängers überprüfen. Weitere Informationen finden Sie unter Ausnahmebehandlung (Task Parallel Library).
Abbrechen einer Fortsetzung
Eine Fortsetzung geht in den folgenden Fällen in den Zustand Canceled über:
Wenn aufgrund einer Abbruchanforderung eine OperationCanceledException ausgelöst wird. Wenn die Ausnahme das gleiche Token enthält, das an die Fortsetzung übergeben wurde, wird dies als Bestätigung des kooperativen Abbruchs interpretiert, wie dies bei allen Aufgaben der Fall ist.
Wenn an die Fortsetzung ein System.Threading.CancellationToken als Argument übergeben wurde und die IsCancellationRequested-Eigenschaft des Tokens true (True) ist, bevor die Fortsetzung ausgeführt wird. In diesem Fall wird die Fortsetzung nicht gestartet und geht direkt in den Zustand Canceled über.
Wenn die Fortsetzung nicht ausgeführt wird, da die in TaskContinuationOptions festgelegte Bedingung nicht erfüllt wurde. Wenn eine Aufgabe beispielsweise in einen Faulted-Zustand übergeht, geht die zugehörige Fortsetzung, die von der NotOnFaulted-Option erstellt wurde, in den Zustand Canceled über und wird nicht ausgeführt.
Um zu verhindern, dass eine Fortsetzung ausgeführt wird, wenn der Vorgänger abgebrochen wurde, geben Sie beim Erstellen der Fortsetzung die NotOnCanceled-Option an.
Wenn eine Aufgabe und die zugehörige Fortsetzung zwei Teile des gleichen logischen Vorgangs sind, können Sie das gleiche Abbruchtoken an beide Aufgaben übergeben, wie im folgenden Beispiel gezeigt.
Dim someCondition As Boolean = True
Dim cts As New CancellationTokenSource
Dim task1 = New Task(Sub()
Dim ct As CancellationToken = cts.Token
While someCondition = True
ct.ThrowIfCancellationRequested()
' Do the work here...
' ...
End While
End Sub,
cts.Token
)
Dim task2 = task1.ContinueWith(Sub(antecedent)
Dim ct As CancellationToken = cts.Token
While someCondition = True
ct.ThrowIfCancellationRequested()
' Do the work here
' ...
End While
End Sub,
cts.Token)
task1.Start()
' ...
' Antecedent and/or continuation will
' respond to this request, depending on when it is made.
cts.Cancel()
Task task = new Task(() =>
{
CancellationToken ct = cts.Token;
while (someCondition)
{
ct.ThrowIfCancellationRequested();
// Do the work.
//...
}
},
cts.Token
);
Task task2 = task.ContinueWith((antecedent) =>
{
CancellationToken ct = cts.Token;
while (someCondition)
{
ct.ThrowIfCancellationRequested();
// Do the work.
//...
}
},
cts.Token);
task.Start();
//...
// Antecedent and/or continuation will
// respond to this request, depending on when it is made.
cts.Cancel();
Wenn der Vorgänger nicht abgebrochen wurde, kann das Token trotzdem zum Abbrechen der Fortsetzung verwendet werden. Wenn der Vorgänger abgebrochen wurde, wird die Fortsetzung nicht gestartet.
Nachdem eine Fortsetzung in den Zustand Canceled übergegangen ist, wirkt sich dies möglicherweise auf nachfolgende Fortsetzungen aus. Dies ist abhängig von den TaskContinuationOptions, die für die Fortsetzungen angegeben wurden.
Fortsetzungen, die verworfen wurden, werden nicht gestartet.
Fortsetzungen und untergeordnete Aufgaben
Eine Fortsetzung wird erst ausgeführt, wenn der Vorgänger und all zugehörigen untergeordneten Aufgaben abgeschlossen wurden. Die Fortsetzung wartet jedoch nicht, bis getrennte untergeordnete Aufgaben abgeschlossen wurden. Der Endstatus der Vorgängeraufgabe ist vom Endstatus aller zugehörigen untergeordneten Aufgaben abhängig. Der Status getrennter untergeordneter Aufgaben wirkt sich nicht auf das übergeordnete Element aus. Weitere Informationen finden Sie unter Geschachtelte Aufgaben und untergeordnete Aufgaben.
Behandeln von durch Fortsetzungen ausgelösten Ausnahmen
Eine Vorgänger-Fortsetzung-Beziehung ist keine Beziehung zwischen übergeordneten und untergeordneten Elementen. Durch Fortsetzungen ausgelöste Ausnahmen werden nicht an den Vorgänger weitergegeben. Behandeln Sie Ausnahmen, die von Fortsetzungen ausgelöst wurden, daher wie bei allen anderen Aufgaben (siehe unten).
- Verwenden Sie die Methode Wait, WaitAll oder WaitAny bzw. die generische Entsprechung, um auf die Fortsetzung zu warten. Sie können in der gleichen try-Anweisung (Try in Visual Basic) auf einen Vorgänger und seine Fortsetzungen warten, wie im folgenden Beispiel gezeigt.
Dim task1 = Task(Of Integer).Factory.StartNew(Function()
Return 54
End Function)
Dim continuation = task1.ContinueWith(Sub(antecedent)
Console.WriteLine("continuation {0}", antecedent.Result)
Throw New InvalidOperationException()
End Sub)
Try
task1.Wait()
continuation.Wait()
Catch ae As AggregateException
For Each ex In ae.InnerExceptions
Console.WriteLine(ex.Message)
Next
End Try
Console.WriteLine("Exception handled. Let's move on.")
var t = Task<int>.Factory.StartNew(() => 54);
var c = t.ContinueWith((antecedent) =>
{
Console.WriteLine("continuation {0}", antecedent.Result);
throw new InvalidOperationException();
});
try
{
t.Wait();
c.Wait();
}
catch (AggregateException ae)
{
foreach(var e in ae.InnerExceptions)
Console.WriteLine(e.Message);
}
Console.WriteLine("Exception handled. Let's move on.");
Verwenden Sie eine zweite Fortsetzung, um die Exception-Eigenschaft der ersten Fortsetzung zu beachten. Weitere Informationen finden Sie unter Ausnahmebehandlung (Task Parallel Library) und Gewusst wie: Behandeln von Ausnahmen, die von Aufgaben ausgelöst werden.
Wenn die Fortsetzung eine untergeordnete Aufgabe ist und mit der AttachedToParent-Option erstellt wurde, werden die zugehörigen Ausnahmen vom übergeordneten Element an den aufrufenden Thread zurückgegeben, wie dies auch bei allen anderen angefügten untergeordneten Element der Fall ist. Weitere Informationen finden Sie unter Geschachtelte Aufgaben und untergeordnete Aufgaben.
Siehe auch
Konzepte
Änderungsprotokoll
Datum |
Versionsgeschichte |
Grund |
---|---|---|
Juni 2010 |
Hinweis über asynchrones Verhalten von Fortsetzungen hinzugefügt. |
Kundenfeedback. |