Task Parallel Library | Continuation Tasks (.NET Framework 4.0)

This article illustrates a simple API exposed by the Task (or Task<TResult> class) which allows arbitrary level of chaining of tasks to execute one after another automatically.

The Task class provides a few methods that allow arbitrary chaining of tasks. One such method is ContinueWith.

ContinueWith:
In its simplest form, here is how it looks:

public Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>,
TNewResult> continuationFunction)

This creates a continuation task hat executes when Task<TResult> completes. It takes in a Func delegate that represents the method to execute when Task<TResult> completes. It returns an instance of Task<TNewResult> which represents the continuation task. TResult and TNewResult represent return types for first and continuation tasks respectively.
Since, Task<TNewResult> gets returned from this operation, one can chain arbitrary number of tasks.

Consider the following method:

static string[] CreateWordArray()
{
var builder = new StringBuilder();
builder.Append("The Agency has an application that generates real time data from Joe’s brain. ");
builder.Append("Joe is an active member of the Cobra gang which harbors destructive intentions. ");
builder.Append("Now that The Agency can see data from Joe’s brain, there’s too much of it for them to analyze. ");
builder.Append("They want to focus only on his destructive intentions. ");
builder.Append("For example, they don’t want to know about his fitness plans, but certainly about his bank robbery plans. ");
builder.Append("The application needs to provide relevant search and view capabilities on that data so that it makes sense to ");
builder.Append("The Agency leads. Tim, the chief architect of the application has reached out to you ");
builder.Append("(one of the rock stars in his team) to implement the searching and viewing features. ");
builder.Append("Time’s running fast as The Agency needs to know about Joe’s intentions before he starts executing them ");
builder.Append("as they say “prevention is better than curing”. So, pull up your socks and get going.");
var s = builder.ToString();

// Separate string into an array of words, removing some common punctuation.
return s.Split(
new char[] { ' ', '\u000A', ',', '.', ';', ':', '-', '_', '/' },
StringSplitOptions.RemoveEmptyEntries);
}

This is a method that returns an array of strings containing all words from a text snippet. Here is a method that pulls out the longest word from this array:

private static string GetLongestWord(string[] words)
{
return words.OrderByDescending(w => w.Length).First();
}

Now, we want to put this act of creating the word array as well as getting the longest word in a separate task. However, the act of pulling out the longest word can only happen after the word array gets created. We can use ContinueWith method to create that dependency as follows:

private static void DemonstrateContinueWith()
{
Console.WriteLine();
Console.WriteLine("Simple chaining of continuation");
Console.WriteLine();

var task1 = new Task<string[]>(() => CreateWordArray());
var task2 = task1.ContinueWith(task => GetLongestWord(task.Result));

task1.Start();
Console.WriteLine("The longest word is {0}", task2.Result);
}

We first create an instance of Task<string[]> to create the word array. We call ContinueWith on that task and pass a delegate that takes in the first task and invokes the method to get the longest word. Please note how we pass the result of the first task as an argument of GetLongestWord method. Having the precedent task as an argument of the delegate allows one to use its properties (result, state or any other attribute) in the second task, thereby allowing passing on context.
There is another overload of ContinueWith that takes in instance of the enum TaskContinuationOptions. There are multiple values that can be combined to achieve the desired behavior. Some of the few available options are NotOnCanceled (enforcing that the continuation should not be scheduled if its antecedent gets cancelled), OnlyOnCanceled does the opposite. Similarly NotOnFaulted and OnlyOnFaulted options indicate that the continuation should not (or should) be scheduled when the antecedent throws an unhandled exception. There are many more available.

Some other overloads of ContinueWith allows you to pass on a cancellation token to the continuation operation thereby allowing it to be cancelled.
But what if we want to schedule a continuation operation that should only execute after a bunch of tasks complete?

ContinueWhenAll:
This method (defined by the TaskFactory class) allows scheduling a continuation after completion of more than one task. In its simplest form, its signature looks like:

public Task ContinueWhenAll(Task[] tasks, Action<Task[]> continuationAction)

This takes in an array of tasks (the antecedents) and an Action<Task[]> that identifies the continuation operation. The action takes in an array of the antecedents as an argument. The method returns a Task. Let’s extend the earlier example to demonstrate this.
Let’s add two more operations on the array of strings. One that calculates most commonly occurring words:

private static IEnumerable<string> GetMostCommonWords(string[] words)
{
return
(from word in words
where word.Length > 6
group word by word into g
orderby g.Count() descending
select g.Key).Take(10);
}

Another takes in a single word and counts number of occurrences of it within the array of words:

private static int GetCountForWord(string[] words, string term)
{
return words.Where(word => word.ToUpper().Contains(term.ToUpper())).Count();
}

We want to display a summary of all the three operations (i.e. getting the longest word, most commonly occurring words, and number of occurrences of a given word), but that can happen only after all the three complete. The obvious option is to do all these sequentially, but that doesn’t allow us executing the individual operations in parallel. Here’s how we can use ContinueWhenAll to achieve this:

private static void DemonstrateContinueWhenAll()
{
Console.WriteLine();
Console.WriteLine("Continue when all");
Console.WriteLine();

var words = CreateWordArray();

var finalTask = Task.Factory.ContinueWhenAll(
new Task[] { Task.Factory.StartNew(() => GetLongestWord(words)),
Task.Factory.StartNew(() => GetMostCommonWords(words)),
Task.Factory.StartNew(() => GetCountForWord(words, "Agency"))},

tasks =>
{
//Prints the longest word
Console.WriteLine("Task 1 -- The longest word is {0}", ((Task<string>)tasks[0]).Result);

var commonWords = ((Task<IEnumerable<string>>)tasks[1]).Result;

//Prints the common words
StringBuilder sb = new StringBuilder();
sb.AppendLine("Task 2 -- The most common words are:");
foreach (var v in commonWords)
{
sb.AppendLine("  " + v);
}
Console.WriteLine(sb.ToString());

//Prints the number of occurrences of a given word
Console.WriteLine(@"Task 3 -- The word Agency occurs {0} times.", ((Task<int>)tasks[2]).Result);
}

Here we get the list of words first. We then call TaskFactory.ContiueWhenAll method with an array of three tasks (one each for all the three word operations) and a Func delegate that takes that array of tasks. In that delegate, we access the results of all the three antecedent tasks to build our summary result.

Here we dealt with untyped tasks, but there are other overloads of ContinueWhenAll which allow us to use strong return types for antecedents as well as the continuation. Yet other overloads exist (similar to ContinueWith) that allow passing TaskContinuationOptions and cancellation tokens.

ContinueWhenAny:
As you might have already guessed, different overloads of this method (similar to ContinueWhenAll) allow executing a continuation when any of the antecedents complete.

By Indranil Chatterjee   Popularity  (3235 Views)