BlackWaspTM

This web site uses cookies. By using the site you accept the cookie policy.This message is for compliance with the UK ICO law.

Parallel and Asynchronous
.NET 4.0+

Continuation Tasks

The eleventh part of the Parallel Programming in .NET tutorial considers the use of continuation tasks. These are parallel tasks that start automatically when one or more other tasks complete, allowing a chain of dependent tasks to be executed correctly.

Chaining Parallel Tasks

When you are writing software that has tasks that execute in parallel, it is common to have some parallel tasks that depend upon the results of others. These tasks should not be started until the earlier tasks, known as antecedents, have completed. Before the introduction of the Task Parallel Library (TPL), this type of interdependent thread execution would be controlled using callbacks. This is where a method is called and one of its parameters is provided a delegate to execute when the task completes.

Although callbacks provide a viable solution to many parallel programming dependency problems, they can become complex very quickly. Additional boilerplate code is required to control the multithreading, which can lead to messy code. This problem is exacerbated when you have complex dependencies between tasks. For example, when one task must be executed only after several others have completed.

When using the TPL and the Task classes, a simpler solution exists in continuation tasks. These tasks are linked to their antecedents and are automatically started after the earlier tasks complete. In this article we will look at the basics of continuations. We will be using classes from the System.Threading and System.Threading.Tasks namespaces, so if you wish to recreate the examples you should create a new console application and add the following using directives to the code:

using System.Threading;
using System.Threading.Tasks;

Creating Continuation Tasks

Continuation tasks are generally created using the ContinueWith method of an existing Task instance. This method accepts a single parameter that defines the task to be executed after the antecedent completes. The parameter is of the generic type Action<Task>. You can define it using a lambda expression that receives a single Task parameter. When executed, the Task parameter provides the antecedent task. The ContinueWith method returns the new continuation task so that you can examine its properties or wait for it to complete.

The syntax for the ContinueWith method is as follows:

Task continuation = firstTask.ContinueWith(antecedent => { /* functionality */ });

Let's create our first example. In the code below we are simulating two data reads, from a database or another data store. The first task is defined as we have seen earlier in the tutorial. It simulates reading user data in order to obtain a user ID, which is held in the userID variable. The second task is a continuation. It simulates loading the user's permission information, using the ID retrieved in the antecedent task. In this case the lambda expression's parameter remains unused. The Wait method of the continuation is called to ensure that both tasks are complete before the final message is displayed.

When you run the code you can see that the loadUserPermissionsTask does not start until the loadUserDataTask is completed.

string userID = null;

var loadUserDataTask = new Task(() =>
{
    Console.WriteLine("Loading User Data");
    Thread.Sleep(2000);
    userID = "1234";
    Console.WriteLine("User data loaded");
});

var loadUserPermissionsTask = loadUserDataTask.ContinueWith(t =>
{
    Console.WriteLine("Loading User Permissions for user {0}", userID);
    Thread.Sleep(2000);
    Console.WriteLine("User permissions loaded");
});

loadUserDataTask.Start();
loadUserPermissionsTask.Wait();

Console.WriteLine("CRM Application Loaded");

loadUserDataTask.Dispose();
loadUserPermissionsTask.Dispose();

/* OUTPUT

Loading User Data
User data loaded
Loading User Permissions for user 1234
User permissions loaded
CRM Application Loaded

*/

Using Task Results in Continuations

The previous example used the userID variable to hold the user's ID, enabling it to be set by one task and used by another. However, the userID did not need to be defined outside of the parallel tasks. It would be cleaner to return the ID as the first parallel task's result and use it in the continuation. As mentioned earlier, the antecedent task is passed to the continuation in the lambda expression's parameter. This means that moving the user ID into the scope of the tasks is easily achieved.

The modified code below changes the initial task to return the user ID. The second task obtains the value by reading the Result property of the first task via the t parameter of the lambda. The continuation also returns the user ID so that it may be used in the final message. In this case the Wait call has been removed as waiting is implicit when reading the continuation's Result property.

var loadUserDataTask = new Task<string>(() =>
{
    Console.WriteLine("Loading User Data");
    Thread.Sleep(2000);
    Console.WriteLine("User data loaded");
    return "1234";
});

var loadUserPermissionsTask = loadUserDataTask.ContinueWith(t =>
{
    Console.WriteLine("Loading User Permissions for user {0}", t.Result);
    Thread.Sleep(2000);
    Console.WriteLine("User permissions loaded");
    return "Admin";
});

loadUserDataTask.Start();

Console.WriteLine("CRM Application Loaded for {0}", loadUserPermissionsTask.Result);

loadUserDataTask.Dispose();
loadUserPermissionsTask.Dispose();

/* OUTPUT

Loading User Data
User data loaded
Loading User Permissions for user 1234
User permissions loaded
CRM Application Loaded for Admin

*/
24 October 2011