Advanced Task Parallel Library Continuations

by Jeffrey Juday

Learn how to build Task Parallel Library Continuation with multiple Antecedents.

.NET Parallel Programming involves more than parallel workloads and concurrency safe data structures. Task Parallel Library (TPL) also features patterns and data structures for ordering and arranging a workload's execution. TPL Continuations play a big part in ordering and arranging a parallel workload. TPL encapsulates a workload and the result of running the workload in a Task. Continuations spawn new Tasks in response to the completion of a prior Task. A prior article demonstrated how to make a Task's execution dependent on the success or failure of another Task. The paragraphs below will take this concept a step further, demonstrating how to structure Task execution with multiple Task dependencies.


An introduction to Continuations is beyond the scope of this article, but a short introduction can be found here http://www.codeguru.com/columns/experts/article.php/c18361/Microsoft-NET-Framework-40-Task-Parallel-Library-Continuations.htm. Sample Continuation code appears below.

           Task.ContinueWith((t) =>
                Console.WriteLine("This continuation also ran.");

As stated earlier, Continuations are Tasks that execute in response to the result of another Task. The prior completed Task is often called an Antecedent. Continuations can execute when a Task completes or, for example, when a Task Faults. Like other running Tasks, Continuations can be LongRunning and can be configured to execute on the same Thread as the Antecedent Task.

Continuations can also have multiple Antecedents, but to configure multiple Antecedents a developer needs the Factory Property on the Task class.

Task Factory Property

Factory is a static property on the Task class. The Factory property is a TaskFactory class. Some methods on the TaskFactory class appear below.

public Task ContinueWhenAll<TAntecedentResult>(Task<TAntecedentResult>[] tasks, Action<Task<TAntecedentResult>[]> continuationAction);
public Task ContinueWhenAll(Task[] tasks, Action<Task[]> continuationAction);
public Task ContinueWhenAll(Task[] tasks, Action<Task[]> continuationAction, TaskContinuationOptions continuationOptions);
public Task ContinueWhenAny<TAntecedentResult>(Task<TAntecedentResult>[] tasks, Action<Task<TAntecedentResult>> continuationAction, CancellationToken cancellationToken);
public Task<TResult> StartNew<TResult>(Func<TResult> function);
public Task<TResult> StartNew<TResult>(Func<object, TResult> function, object state);

StartNew creates and starts a new Task. TaskFactory is aptly named. Observe how the methods above all return a Task class. As stated earlier Continuations are Tasks that execute in response to the result of an Antecedent Task. Tasks with multiple Antecedents are created from ContinueWhenAny and ContinueWhenAll methods. Both methods and related overloads accept an array of Tasks. For example: ContinueWhenAll creates a Task when an array of Tasks completes.

ContinueWhenAny and ContinueWhenAll both leverage other parts of TPL like Cancellations and TaskCreationOptions. Like other Continuations, Tasks created with ContinueWhenAll and ContinueWhenAny can create a LongRunning or ParentTask.

The remainder of this article will demonstrate how to put the ContinueWhenAll to work.

Array of Tasks and TaskCompletionSource

The sample code below creates an array of Tasks each configured for a Llamda payload.

            var antecedents = new Task[3]
            { new Task (() =>
                    SpinWait.SpinUntil(()=>{return false;},1000);
                    Console.WriteLine("Completed 1");
                ,new Task(() =>
                    SpinWait.SpinUntil(()=>{return false;},2000);
                    Console.WriteLine("Completed 2");
                ,new Task(() =>
                    SpinWait.SpinUntil(()=>{return false;},3000);
                    Console.WriteLine("Completed 3");

Continuation Antecedents are not limited to Tasks. Tasks store a work payload and the result of the executed payload. Tasks also maintain the status of the running payload and are the core TPL component. Not all Tasks, however, have an Action or Func<T> payload. Some Tasks can be manipulated using the TaskCompletionSource. Below are some methods and properties of the TaskCompletionSource.

    public class TaskCompletionSource<TResult>
        public TaskCompletionSource();

        public void SetCanceled();
        public void SetException(Exception exception);
        public void SetResult(TResult result);
        public bool TrySetCanceled();
        public bool TrySetException(Exception exception);
        public bool TrySetResult(TResult result);

Methods prefixed with "Set" transition the underlying Task to the appropriate state and generate an Exception if the Task is already in a completed state. Methods prefixed with "Try" attempt to transition state, but return false if the Task is in a completed state. TaskCompletionSource allows a developer to, for example, join code to a Continuation without directly creating and starting a Task. The sample code below demonstrates how to combine the TaskCompletionSource with the array of Tasks from the example above.

            var tcs = new TaskCompletionSource<object>();
            var fullList = new List<Task>();

The Task Property on TaskCompletionSource is the key. Task Property is a Task without an Action or Func<T> payload. "Set" and "Try" method on the TaskCompletionSource transition the Task to the appropriate state. In the sample, ContinueWhenAll creates a Task when the array of Tasks complete and the Task behind the TaskCompletionSource transitions to a completed state.


A Sample ContinueWhenAll that accepts the array of Tasks and TaskCompletionSource Task appears below.

            Task.Factory.ContinueWhenAll(fullList.ToArray(), (tasks) =>
                Console.WriteLine("ContinueWhenAll started...");
                foreach (var t in tasks)
                    if (t.IsCanceled)
                    { Console.WriteLine("Antecedent was cancelled"); }
            foreach (var t in antecedents) { t.Start(); }
            SpinWait.SpinUntil(() => { return false; }, 5000);

One of the ContinueWhenAll parameter is an Action or Func<T> that accepts an array of Tasks. A Continuation is often dependent on the Results of its Antecedents. With multiple Antecedents the results are an array. Antecedents can fault or be cancelled so a Continuation can observe Exceptions or cancellations just like a Continuation attached to a single Task.

ContinueWhenAll doesn't create the Task until all Tasks have completed. In the example all Tasks quickly complete and after a short delay the TaskCompletionSource transitions its Task to the cancelled state.


More advanced Continuations with multiple Antecedents can be configured with the Task's Factory property. Continuations are not limited to simply Tasks. The TaskCompletionSource can also be a useful resource.


"Microsoft .NET Framework 4.0 Task Parallel Library Continuations"

This article was originally published on Friday Sep 9th 2011
Mobile Site | Full Site