My two cents on task continuation in .Net

Create continuations using the Task Parallel Library that can run immediately after the execution of the antecedent is complete or to chain tasks as continuations

taskcontinuation in .net
Joydip Kanjilal

A continuation is like a delegate that can be attached to a task so that you can execute it once the task has finished its execution. When working with asynchronous programming in .Net, you might often need to chain tasks as continuations or pass data to a task based on the data returned by another task. To achieve this, you would need to leverage continuations.

Continuations in the Task Parallel Library

Before TPL was around, continuations were performed using callback methods. A continuation is just another task and doesn't block the thread on which it is called. While continuation in TPL is simple to implement, here's a quick look at the benefits that task continuations have to offer:

  • Invoke one or more continuations using the same antecedent
  • Invoke a continuation when one or more antecedents have completed their execution
  • Specify certain conditions based on which the continuations should be executed
  • Chain continuations and pass data from an antecedent task to a continuation task

Note that the ContinueWith method returns a task instance allowing you to have a chain of ContinueWith calls. Also, unlike the await keyword, the ContinueWith method doesn't preserve the state and is executed by the default thread scheduler (if you don't provide one) although you can also specify your custom scheduler when invoking the ContinueWith method.

Creating continuation tasks using ContinueWith method

You can create continuation tasks in TPL using the ContinueWith method on a Task. It should be noted that this method accepts just one parameter -- the task that needs to be executed when the antecedent has completed its execution. The task instance on which the ContinueWith method is invoked is known as the antecedent task. Instead, the task that is executed by the ContinueWith method after execution of its antecedent task is complete is known as the continuation task. Let's look at an example.

Here's how the syntax of the ContinueWith method looks:

public Task<TResult> ContinueWith<TResult>(

              Func<Task, TResult> continuationFunction

)

The ContinueWith method in turn returns another continuation task. Here's a code snippet that illustrates how you can create a continuation task using one antecedent:

 public static void Main()

        {

            Task<string> initialTask = Task.Run(() => DateTime.Today.ToShortDateString());

            Task continuation = initialTask.ContinueWith(antecedentTask =>

            Console.WriteLine("Today is {0}.", antecedentTask.Result));

            Console.Read();

        }

Refer to the code snippet given above. The first task is executed using a call to the Task.Run method. This is the antecedent task in our example. Once the execution of the antecedent task is complete, the continuation task is executed in the next statement.

Note that you can also create a continuation that is to be executed when one or more antecedents have completed their execution. To execute a continuation when all the antecedent tasks are complete, you can take advantage of the Task.WhenAll or the TaskFactory.ContinueWhenAll methods. Alternatively, you can execute a continuation when any of the antecedent tasks are complete using the Task.WhenAny or TaskFactory.ContinueWhenAny methods.

It should be noted that none of these methods block the calling thread. Although there are several methods (ie., Wait, WaitAll, WaitAny, Result, and GetAwaiter().GetResult()) that allow you to wait till the execution of a task is complete, all of these synchronously block the calling thread.

You can also specify continuation options when using the ContinueWith, ContinueWhenAll methods. To do this, you would need to take advantage of the Task.TaskContinuationOptions enumeration of the System.Threading namespace. As an example, you can ensure that a continuation executes only after the execution of the antecedent has completed its execution.

var continuationTask = task.ContinueWith( (antecedentTask) => {

 Console.WriteLine("This will execute only after the antecedent has completed its execution."); } , TaskContinuationOptions.OnlyOnRanToCompletion);

Here's another example that illustrates how task continuation options can be used to execute a continuation only after the antecedent has completed its execution.

public static void Main()

        {

            var task = Task.Run(() => {

               return "Joydip";

            });

            var t = task.ContinueWith((antecedent) => {

                Console.WriteLine("Hello {0}", antecedent.Result);

            }, TaskContinuationOptions.OnlyOnRanToCompletion);

            Console.Read();

        }

If you use ContinueWith without mentioning any special flag, the continuation will always be executed regardless of the state of the antecedent, i.e., the continuation will be executed even if the antecedent has completed its executed in a faulted or a cancelled state.

As a side note, let me tell you that it is advisable to avoid using ContinueWith in your applications. In doing so, you can avoid threading issues and also avoid allocating an extra task instance when it is not needed in your application. You can do away from ContinueWith by using task helpers. I will discuss more on this in a later post here.

Copyright © 2017 IDG Communications, Inc.