Advanced Task Parallel Library Continuations

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

Continuations

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>();
 
            fullList.AddRange(antecedents);
            fullList.Add(tcs.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.

ContinueWhenAll

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);
            tcs.SetCanceled();
 

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.

Summary

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.

Sources

Microsoft
.NET Framework 4.0 Task Parallel Library Continuations

More by Author

Must Read