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

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

  • Comments 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 former is desired, grant access to the CancellationToken only.

3.       TPL now uses the new .NET 4 cancellation types.  You can use the same CancellationToken{Source} to effect cancellation on multiple Tasks, Parallel.For calls, PLINQ queries, and more, all at once.  This provides both better composition and better performance.

Finally, there are other advantages to this model.  One is that it is much easier to organize cancellation sets within a large nest of Tasks; just create the right number of tokens and pass them around accordingly.  Before, this scenario would have required tedious bookkeeping of many Task references and/or careful architecting of cancellation chains using TaskCreationOptions.RespectParentCancellation.

That’s it for now.  The next post in the series will discuss a change regarding Tasks and parent/child relationships.

Leave a Comment
  • Please add 3 and 4 and type the answer here:
  • Post
  • Great stuff overall, but two thoughts come to mind:

    1. Throwing to acknowledge cancellation has a don't'-use-exceptions-where-logic-and-result-values-would-suffice smell that suggests changing (or adding an overload to) StartNew that takes a Func<bool> where the func should return true if cancelled. A variant on this might instead take an Action<T> where T is some type that can capture cancellation status by way of a method call or property setter.

    2. The manual creation of the token source and token, while useful in cases that farm out lots of work, suggests a StartNew overload that takes an Action<CancellationToken> (or Func<CancellationToken, bool> :) ) for use in those cases of spinning up just one piece of cancellable parallel work.

  • Hi Jeremy,

    Thanks for your thoughts!  They got me thinking.

    Re: 1. Throwing to acknowledge cancellation...

    We need to support Func<TResult> so adding overloads that took Func<bool> would get hairy.  Also, here are a few reasons why we like the current approach:

    - It's composable.  For example, if you wrap a Parallel.For call in a Task and cancel both with the same token, it just works, because the OperationCanceledException that is thrown by Parallel.For acknowledges cancellation for the Task.  Using bools or some T that can capture cancellation status would require a try/catch block around the loop and separately acknowledging cancellation for the Task.

    - Cancellation is an abnormal way to terminate.  An exception propagates up the call stack to notify all levels of the hierarchy of this.  A bool-based approach would require additional logic at each level.

    Re: 2. auto-generating the token source and token

    I agree that that would be a little more convenient, but it doesn't seem like a huge hassle to create a token source (the token is created in the same operation).  In any case, it should be easy to create helper methods for this:

    public static Task StartNewCancelable(

       Action<CancellationTokenSource> body)

    {

       var cts = new CancellationTokenSource();

       return Task.Factory.StartNew(

           () => action(cts), cts.Token);

    }

    Or:

    public static Task StartNewCancelable(

       Func<CancellationToken, bool> body)

    {

       var cts = new CancellationTokenSource();

       return Task.Factory.StartNew(() =>

       {

           if(!body(cts.Token))

           {

               cts.Cancel();

               cts.Token.ThrowIfCancellationRequested();

           }

       }, cts.Token);

    }

    Thanks,

    Danny

  • All true, and it is certainly easy enough to layer up convenience helpers to suit one's one situation.

Page 1 of 1 (3 items)