What’s new in Beta 2 for the Task Parallel Library? (Part 1/3)

Related posts:

Visual Studio 2010 and .NET 4 Beta 2 is here!  In terms of completeness and readiness for production coding, Beta 2 promises to be much better than Beta 1, and TPL is one component that delivers significant improvements over what was previously available.  To get you excited about it, this series of posts details key additions and changes for Beta 2.  Enjoy!

In this post, we’re talking about cancellation.  A few months ago, our flurry of “What’s new in Beta 1” posts hinted at a new cancellation model and promised more information about it.  We made good on that promise with .NET 4 Cancellation Framework, and this post goes further to explain how TPL, specifically, has fully adopted the new model.

The Old Way (Beta 1 and before)

Let’s consider the following code to review the old TPL cancellation model.

Task myTask = Task.Factory.StartNew(() =>

{

    for (; ; )

    {

        if (Task.Current.IsCancellationRequested)

        {

            Task.Current.AcknowledgeCancellation();

            return;

        }

       

    }

});

 

// Elsewhere.

myTask.Cancel();

 

myTask just loops infinitely, checking its IsCancellationRequested property to see if it has been canceled.  Elsewhere, cancellation is requested on myTask using the Cancel method.  At that point, myTask agrees to get canceled by calling its AcknowledgeCancellation method and returning.  This is all necessary, because Task cancellation is cooperative;  to enter the Canceled state, outside logic must request cancellation and the Task must acknowledge that cancellation request.  However, note that cooperative cancellation is only relevant for already running Tasks.  If a Task’s Cancel method is called before it is in the Running state, it will transition directly into the Canceled state.

Waiting on a canceled Task results in a TaskCanceledException wrapped in an AggregateException, hence the try/catch block.  Executing this code prints “Canceled” to the console, indicating that myTask was successfully canceled.

This approach works, but we identified a number of problems with it including the following:

1.       The cancellation model required exposing a Task.Current static property.  This leaks implementation details from libraries that utilize Tasks internally; any code called from the Task can muck with the current Task or take dependencies on its existence.  Imagine calling into 3rd party code and having that code cancel your Tasks, schedule continuations off of them, etc.

2.       Anyone with a Task’s reference can request cancellation on it.  In many scenarios, it is valuable to separate the ability to check for cancellation and the ability to actually request cancellation.

3.       From a cancellation perspective, Tasks didn’t compose well with other APIs that were cancelable, such as executing a cancelable PLINQ query inside of a Task.

4.       Tasks were often more expensive than they needed to be, due to needing to track extra cancellation state per Task.

The New Way (Beta 2 and beyond)

The new TPL cancellation model is centered around two types: CancellationTokenSource and CancellationToken.  You can read up about them in the post linked to above, but here is a simplified overview of their APIs:

namespace System.Threading
{
    public sealed class CancellationTokenSource
    {
        public void Cancel();
        public CancellationToken Token { get; }
       
    }

    public struct CancellationToken
    {
        public Boolean IsCancellationRequested { get; }
        public void ThrowIfCancellationRequested();
       
    }
}

Here’s the gist.  A CancellationTokenSource contains a CancellationToken, and it can request cancellation on that token using a Cancel method.  A CancellationToken can only check if cancellation has been requested on it.  Ignore the ThrowIfCancellationRequested method for now; we’ll see why it’s handy later.

Adopting the new model involved not only adding support for these two types, but also ripping out the old model.  Here’s a summary of the changes:

·         All cancellation-related APIs on the Task class were removed (Cancel, AcknowledgeCancellation, IsCancellationRequested, etc)

·         Other APIs that were no longer relevant were removed (Task.Current, Task.Parent, TaskCreationOptions.RespectParentCancellation, etc)

·         Overloads that accept CancellationToken were added to many methods (StartNew, ContinueWith, etc)

And now, here’s a table that outlines how achieving cancellation in TPL has changed:

Action

Old Model

New Model

To set up cancellation

Just create a Task

Create a CancellationTokenSource and pass its Token to an API that creates a Task.

To check if cancellation has been requested

Check the IsCancellationRequested property on the relevant Task

Check the IsCancellationRequested property on the CancellationToken that was passed to the API that created the Task

To acknowledge cancellation

Check to ensure that IsCancellationRequested is true, then call the AcknowledgeCancellation() method on the relevant Task

Throw an OperationCanceledException with the task’s CancellationToken

To cancel a tree of tasks

Create all of the tasks as attached tasks and with the RespectParentCancellation flag set, then cancel the root task.

Pass the same CancellationToken to all Tasks, then cancel the associated CancellationTokenSource

 

To see how this all works, let’s rewrite the code above for Beta 2.

CancellationTokenSource cts = new CancellationTokenSource();

CancellationToken token = cts.Token;

 

Task myTask = Task.Factory.StartNew(() =>

{

    for (; ; )

    {

        token.ThrowIfCancellationRequested();

       

    }

}, token);

 

// Elsewhere.

cts.Cancel();

 

A CancellationTokenSource (cts) is initialized, and a CancellationToken (token) is initialized to cts’s Token.  myTask still loops indefinitely, but now it calls ThrowIfCancellationRequested.  This method just checks the IsCancellationRequested property on a CancellationToken.  If true, it throws  an OperationCanceledException(token), which is the way to acknowledge cancellation in the new model.  Elsewhere, Cancel is called on cts to request cancellation on myTask.

The new model addresses all of the problems with the old model listed above:

1.       No more Task.Current (Task.Parent was removed too, as it could be used to get at the current Task).

2.       The ability to check for cancellation requests may now be separated from the ability to request cancellation.  If only the