Continuation Tasks
In asynchronous programming, it is very common for one asynchronous operation, on completion, to invoke a second operation and pass data to it. Traditionally, this has been done by using callback methods. In the Task Parallel Library, the same functionality is provided by continuation tasks. A continuation task (also known just as a continuation) is an asynchronous task that is invoked by another task, which is known as the antecedent, when the antecedent completes.
Continuations are relatively easy to use, but are nevertheless quite powerful and flexible. For example, you can:
pass data from the antecedent to the continuation
specify the precise conditions under which the continuation will be invoked or not invoked
cancel a continuation either before it starts or cooperatively while it is running
provide hints about how the continuation should be scheduled
invoke multiple continuations from the same antecedent
invoke one continuation when all or any of multiple antecedents complete
chain continuations one after another to any arbitrary length
use a continuation to handle exceptions thrown by the antecedent
Create continuations by using the Task.ContinueWith method. The following example shows the basic pattern, (for clarity, exception handling is omitted).
' 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);
You can also create a multi-task continuation that will run when any or all of an array of tasks have completed, as shown in the following example.
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();
A continuation is created in the WaitingForActivation state and therefore it can only be started by its antecedent task. To call Task.Start on a continuation in user code raises a System.InvalidOperationException.
A continuation is itself a Task and does not block the thread on which it is started. Use the Wait method to block until the continuation task completes.
Continuation Options
When you create a single-task continuation, you can use a ContinueWith overload that takes the System.Threading.Tasks.TaskContinuationOptions enumeration to specify the conditions under which the antecedent task is to start the continuation. For example, you can specify that the continuation is to run only if the antecedent ran to completion, or only if it completed in a faulted state, and so on. If the condition is not true when the antecedent is ready to invoke the continuation, the continuation transitions directly the Canceled state and cannot be started after that. If you specify any of the NotOn or OnlyOn options with a multi-task continuation, an exception will be thrown at run time.
The System.Threading.Tasks.TaskContinuationOptions enumeration also includes the same options as the System.Threading.Tasks.TaskCreationOptions enumeration. AttachedToParent, LongRunning, and PreferFairness have the same meanings and values in both enumeration types. These options can be used with multi-task continuations.
The following table lists all of the values in TaskContinuationOptions.
Element |
Description |
---|---|
Specifies the default behavior when no TaskContinuationOptions are specified. The continuation will be scheduled when the antecedent completes, regardless of the final status of the antecedent. If the task is a child task, it is created as a detached nested task. |
|
Specifies that the continuation will be scheduled so that tasks scheduled sooner will be more likely to be run sooner, and tasks scheduled later will be more likely to be run later. |
|
Specifies that the continuation will be a long-running, course-grained operation. It provides a hint to the System.Threading.Tasks.TaskScheduler that oversubscription may be warranted. |
|
Specifies that the continuation, if it is a child task, is attached to a parent in the task hierarchy. The continuation is a child task only if its antecedent is also a child task. |
|
Specifies that the continuation should not be scheduled if its antecedent ran to completion. |
|
Specifies that the continuation should not be scheduled if its antecedent threw an unhandled exception. |
|
Specifies that the continuation should not be scheduled if its antecedent was canceled. |
|
Specifies that the continuation should only be scheduled if the antecedent ran to completion. |
|
Specifies that the continuation should be scheduled only if its antecedent threw an unhandled exception. When you use the OnlyOnFaulted option, it is guaranteed that the Exception property in the antecedent is not null. You can use that property to catch the exception and see which exception caused the task to fault. If you do not access the Exception property, the exception will go unhandled. Also, if you attempt to access the Result property of a task that has been canceled or has faulted, a new exception will be raised. |
|
Specifies that the continuation should be scheduled only if its antecedent completes in the Canceled state. |
|
For very short-running continuations. Specifies that the continuation should ideally be run on the same thread that causes the antecedent to transition into its final state. If the antecedent is already complete when the continuation is created, the system will attempt to run the continuation on the thread that creates the continuation. If the antecedent's CancellationTokenSource is disposed in a finally block (Finally in Visual Basic), a continuation with this option will run in that finally block. |
Passing Data to a Continuation
A reference to the antecedent is passed to the user delegate of the continuation as an argument. If the antecedent is a System.Threading.Tasks.Task<TResult>, and the task ran to completion, then the continuation can access the Task<TResult>.Result property of the task. With a multi-task continuation and the Task.WaitAll method, the argument is the array of antecedents. When you use Task.WaitAny, the argument is the first antecedent that completed.
Task<TResult>.Result blocks until the task has completed. However, if the task was canceled or faulted, then Result will throw an exception when your code tries to access it. You can avoid this problem by using the OnlyOnRanToCompletion option, as shown in the following example.
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);
If you want the continuation to run even when the antecedent did not run to completion, then you must guard against the exception. One possible approach is to test the status of the antecedent, and only attempt to access Result if the status is not Faulted or Canceled. You can also examine the Exception property of the antecedent. For more information, see Exception Handling (Task Parallel Library).
Canceling a Continuation
A continuation goes into the Canceled state in these scenarios:
When it throws an OperationCanceledException in response to a cancellation request. Just as with any task, if the exception contains the same token that was passed to the continuation, it is treated as an acknowledgement of cooperative cancellation.
When the continuation was passed a System.Threading.CancellationToken as an argument and the IsCancellationRequested property of the token is true (True)before the continuation runs. In such a case, the continuation does not start and it transitions directly to the Canceled state.
When the continuation never runs because the condition set in its TaskContinuationOptions was not met. For example, if a task goes into a Faulted state, its continuation that was created by the NotOnFaulted option will transition to the Canceled state and will not run.
To prevent a continuation from executing if its antecedent is canceled, specify the NotOnCanceled option when you create the continuation.
If a task and its continuation represent two parts of the same logical operation, you can pass the same cancellation token to both tasks, as shown in the following example.
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();
If the antecedent was not canceled, the token can still be used to cancel the continuation. If the antecedent was canceled, the continuation will not be started.
After a continuation goes into the Canceled state, it may affect continuations that follow, depending on the TaskContinuationOptions that were specified for those continuations.
Continuations that are disposed will not start.
Continuations and Child Tasks
A continuation does not run until the antecedent and all of its attached child tasks have completed. The continuation does not wait for detached child tasks to complete. The final status of the antecedent task is dependent on the final status of any attached child tasks. The status of detached child tasks does not affect the parent. For more information, see Nested Tasks and Child Tasks.
Handling Exceptions Thrown from Continuations
An antecedent-continuation relationship is not a parent-child relationship. Exceptions thrown by continuations are not propagated to the antecedent. Therefore, handle exceptions thrown by continuations as you would handle them in any other task, as follows.
- Use the Wait, WaitAll, or WaitAny method, or the generic counterpart, to wait on the continuation. You can wait for an antecedent and its continuations in the same try (Try in Visual Basic) statement, as shown in the following example.
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.");
Use a second continuation to observe the Exception property of the first continuation. For more information, see Exception Handling (Task Parallel Library) and How to: Handle Exceptions Thrown by Tasks.
If the continuation is a child task and was created by using the AttachedToParent option, then its exceptions will be propagated by the parent back to the calling thread, as is the case in any other attached child. For more information, see Nested Tasks and Child Tasks.