Export (0) Print
Expand All

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 finishes.

Continuations are relatively easy to use, but are nevertheless very 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 as 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 one 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)

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 = antecedents(0).Result + antecedents(1).Result
                                                           Console.WriteLine("The answer is {0}", answer)


                                                       End Sub)
task1.Start()
task2.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 finishes.

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 until it was completed, 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

None

Specifies the default behavior when no TaskContinuationOptions are specified. The continuation will be scheduled when the antecedent finishes, regardless of the final status of the antecedent. If the task is a child task, it is created as a detached nested task.

PreferFairness

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.

LongRunning

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.

AttachedToParent

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.

DenyChildAttach

Specifies that if an inner task specifies the AttachedToParent option, that task will not become an attached child task.

HideScheduler

Specifies that tasks created in this task observe TaskScheduler.Current to be TaskScheduler.Default rather than the scheduler on which this task is running.

NotOnRanToCompletion

Specifies that the continuation should not be scheduled if its antecedent ran until it was completed.

NotOnFaulted

Specifies that the continuation should not be scheduled if its antecedent threw an unhandled exception.

NotOnCanceled

Specifies that the continuation should not be scheduled if its antecedent was canceled.

OnlyOnRanToCompletion

Specifies that the continuation should only be scheduled if the antecedent ran until it was completed.

OnlyOnFaulted

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.

OnlyOnCanceled

Specifies that the continuation should be scheduled only if its antecedent finishes in the Canceled state.

ExecuteSynchronously

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.

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(Of TResult), and the task ran until it was completed, then the continuation can access the Task(Of 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(Of 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)

If you want the continuation to run even when the antecedent did not run until it was completed, 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).

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

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.

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 finish. The final status of the antecedent task depends on the final status of any attached child tasks. The status of detached child tasks does not affect the parent. For more information, see Attached and Detached Child Tasks.

You can associate arbitrary state with a task continuation. The ContinueWith method provides overloaded versions that each take an Object value that represents the state of the continuation. You can later access this state object by using the Task.AsyncState property. This state object is null (Nothing Visual Basic) if you do not provide a value.

Continuation state is useful when you convert existing code that uses the Asynchronous Programming Model (APM) to use the TPL. In the APM, you typically provide object state in the BeginMethod method and later access that state by using the IAsyncResult.AsyncState property. By using the ContinueWith method, you can preserve this state when you convert code that uses the APM to use the TPL.

Continuation state can also be useful when you work with Task objects in the Visual Studio debugger. For example, in the Parallel Tasks window, the Task column displays the string representation of the state object for each task. For more information about the Parallel Tasks window, see Using the Tasks Window.

The following example shows how to use continuation state. This example creates a chain of continuation tasks. Each task provides the current time, a DateTime object, for the state parameter of the ContinueWith method. Each DateTime object represents the time at which the continuation task is created. Each task produces as its result a second DateTime object that represents the time at which the task finishes. After all tasks finish, this example prints to the console the creation time and the time at which each continuation task finishes.

Imports System
Imports System.Collections.Generic
Imports System.Threading
Imports System.Threading.Tasks

' Demonstrates how to associate state with task continuations. 
Friend Class ContinuationState
   ' Simluates a lengthy operation and returns the time at which 
   ' the operation completed. 
   Public Shared Function DoWork() As Date 
      ' Simulate work by suspending the current thread  
      ' for two seconds.
      Thread.Sleep(2000)

      ' Return the current time. 
      Return Date.Now
   End Function 

   Shared Sub Main(ByVal args() As String)
      ' Start a root task that performs work. 
      Dim t As Task(Of Date) = Task(Of Date).Run(Function() DoWork())

      ' Create a chain of continuation tasks, where each task is  
      ' followed by another task that performs work. 
      Dim continuations As New List(Of Task(Of Date))()
      For i As Integer = 0 To 4
         ' Provide the current time as the state of the continuation.
         t = t.ContinueWith(Function() DoWork(), Date.Now)
         continuations.Add(t)
      Next i

      ' Wait for the last task in the chain to complete.
      t.Wait()

      ' Print the creation time of each continuation (the state object) 
      ' and the completion time (the result of that task) to the console. 
      For Each continuation In continuations
         Dim start As Date = CDate(continuation.AsyncState)
         Dim [end] As Date = continuation.Result

         Console.WriteLine("Task was created at {0} and finished at {1}.", start.TimeOfDay, [end].TimeOfDay)
      Next continuation
   End Sub 
End Class 

' Sample output: 
'Task was created at 10:56:21.1561762 and finished at 10:56:25.1672062. 
'Task was created at 10:56:21.1610677 and finished at 10:56:27.1707646. 
'Task was created at 10:56:21.1610677 and finished at 10:56:29.1743230. 
'Task was created at 10:56:21.1610677 and finished at 10:56:31.1779883. 
'Task was created at 10:56:21.1610677 and finished at 10:56:33.1837083. 
'

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.

  1. 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.")
    
  2. 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.

  3. 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 Attached and Detached Child Tasks.

Show:
© 2014 Microsoft