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

            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 = antecedents[0].Result + antecedents[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 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<TResult>, and the task ran until it was completed, 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.

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

CancellationTokenSource cts = new CancellationTokenSource();
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.

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.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

// Demonstrates how to associate state with task continuations. 
class ContinuationState
{
   // Simluates a lengthy operation and returns the time at which 
   // the operation completed. 
   public static DateTime DoWork()
   {
      // Simulate work by suspending the current thread  
      // for two seconds.
      Thread.Sleep(2000);

      // Return the current time. 
      return DateTime.Now;
   }

   static void Main(string[] args)
   {
      // Start a root task that performs work.
      Task<DateTime> t = Task<DateTime>.Run(delegate { return DoWork(); });

      // Create a chain of continuation tasks, where each task is  
      // followed by another task that performs work.
      List<Task<DateTime>> continuations = new List<Task<DateTime>>();
      for (int i = 0; i < 5; i++)
      {
         // Provide the current time as the state of the continuation.
         t = t.ContinueWith(delegate { return DoWork(); }, DateTime.Now);
         continuations.Add(t);
      }

      // 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. 
      foreach (var continuation in continuations)
      {
         DateTime start = (DateTime)continuation.AsyncState;
         DateTime end = continuation.Result;

         Console.WriteLine("Task was created at {0} and finished at {1}.",
            start.TimeOfDay, end.TimeOfDay);
      }
   }
}

/* 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.

    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.");
    
  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.

Was this page helpful?
(1500 characters remaining)
Thank you for your feedback
Show:
© 2014 Microsoft