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

Danny Shih

Related posts:

Last week, we talked about how TPL adopted a new, better cancellation model.  Today, we’ll cover a change that makes Tasks Detached by Default, some ContinueWhenAll/Any Refactoring, and the handy UnobservedTaskException event.

Tasks are Detached by Default

In Beta 2, we have changed an important default.  Tasks are now created as detached (instead of attached) if no options specify otherwise.

Let’s consider the following code to review the difference between attached and detached Tasks.

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

{

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

    {

        DoWork();

    });

});

 

p.Wait();

 

In Beta 1 and before, since the default options are used, ‘c’ is created as a child Task of Task ‘p’, the parent Task; we refer to this as Task ‘c’ being “attached” to Task ‘p’.  This means that the p.Wait() statement will not return until the call to DoWork completes, because parent Tasks do not complete until all of their child Tasks complete.  To opt out of this behavior, a user needs to create ‘c’ with the DetachedFromParent option:

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

    {

        DoWork();

    }, TaskCreationOptions.DetachedFromParent);

 

The original code shown behaves differently in Beta 2.  Now, by default, ‘c’ is not related to ‘p’ (it’s “detached” by default), and the p.Wait() statement will return as soon as ‘p’ completes, regardless of the status of Task ‘c’ and thus regardless of when DoWork returns.  To opt in to the parent/child relationship, a user needs to create ‘c’ with the AttachedToParent option:

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

    {

        DoWork();

    }, TaskCreationOptions.AttachedToParent);

 

Here is a summary of the changes:

·         Removed the DetachedFromParent option

·         Added the AttachedToParent option

·         Changed the default behavior so that Tasks do not enlist in parent/child relationships when no options are specified.

There were a number of reasons why we decided that detached is the correct default and to move forward with this change, including:

·         Many users were using attached Tasks unknowingly.  The vast majority of the time, users create Tasks for simple, asynchronous work.  In such scenarios, parent/child relationships (and the implicit waiting) are not needed.  We found through many interactions that folks were just going with the default options and were accidentally opting in to this behavior.  In the best case, this would only result in a slight performance cost.  In the worst case, this would bring with it incorrect behavior that would lead to difficult to diagnose errors.

·         Easier migration from ThreadPool.QueueUserWorkItem.  Tasks are now the recommended way to queue work to the ThreadPool, but the easiest way to create Tasks resulted in different behavior from QueueUserWorkItem (where there’s no concept of parent/child work items).  This change makes Task.Factory.StartNew (with no options) a true replacement for QueueUserWorkItem.

·         Additional behavior should be opt-in and pay-for-play.  Almost everything in TPL that results in additional behavior is opt-in, e.g. cancellation, LongRunning, PreferFairness.  With the Beta 1 default, users opt-out of parent/child relationships.  In Beta 2, users opt-in, making it consistent.  This makes the extra functionality provided by parent/child relationships pay-for-play, such that you don’t pay the cost for parents implicitly waiting for their children or for exceptions propagating from children to parents unless you need that functionality.

ContinueWhenAny/All Refactoring

We have refactored the set of ContinueWhenAny and ContinueWhenAll overloads to make things more intuitive, consistent, and complete.

To demonstrate the main issue, let’s consider the following overload that was provided in Beta 1.

public class TaskFactory<TResult>

{

    public Task<TNewResult> ContinueWhenAny(

        Task<TResult>[] tasks,

        Func<Task<TResult>, TNewResult> continuationFunction);

}

 

This confused the meaning of TaskFactory<TResult>, which is meant to create tasks of type Task<TResult>.  However, with these overloads, TaskFactory<TResult> could be used to create tasks of type Task<TNewResult>. As an example, consider the code:

Task<int>[] taskOfInts = …;
Task<string> t = Task<int>.Factory.ContinueWhenAll(taskOfInts, _ => “”);

This compiles and works just fine, but the type parameter mismatch (shown in bold) is certainly odd.  To address this, we changed a bunch of overloads, so that instead of taking Task<TResult>s and returning a Task<TNewResult>, they take Task<TAntecedentResult>s and return Task<TResult>s.  For example, the overload that replaced the above is:

public Task<TResult> ContinueWhenAny<TAntecedentResult>(

    Task<TAntecedentResult>[] tasks,

    Func<Task<TAntecedentResult>, TResult> continuationFunction);

 

And the above example becomes:

Task<int>[] taskOfInts = …;
Task<string> t = Task<string>.Factory.ContinueWhenAll(taskOfInts, _ => “”);

In addition to this change, we also added, removed, or modified a number of other overloads to make the set consistent and complete.  Now, the entire set of ContinueWhenAll and ContinueWhenAny overloads follow these clear rules:

·         A TaskFactory creates Tasks, but also provides overloads to create Task<TResult>s.

·         A TaskFactory<TResult> only ever creates Task<TResult>s (never Tasks or Task<TNewResult>s).

UnobservedTaskException event

We’ve added an event that fires for every Task exception that goes unobserved.  Recall that to “observe” a Task’s exceptions, you must either Wait on the Task or access its Exception property after it has completed.  At least one of these actions must be done before the Task object is garbage collected, or its exceptions will propagate (currently this occurs on the finalizer thread).

The new static event resides on the TaskScheduler class, and subscribing to it is straightforward.  Here’s an example to log all unobserved exceptions and mark them as observed (preventing them from being propagated).

TaskScheduler.UnobservedTaskException +=

    (object sender, UnobservedTaskExceptionEventArgs exceptionArgs) =>

    {

        exceptionArgs.SetObserved();

        LogException(exceptionArgs.Exception);

    };

 

Some customers have complained that TPL’s exception policy is too strict.  The UnobservedTaskException event provides an easy way out by allowing you to simply squash all Task exceptions in an application (though using it in this manner is not recommended).  The primary reason that we made the addition was to support host-plugin scenarios where a host application can still be perfectly useful in the presence of some truly harmless exceptions (thrown by buggy plugins).  These scenarios may be achieved using the UnobservedTaskException event in conjunction with AppDomains to sandbox plugins.  Look for a future post that describes this in more detail!

We’re done for now!  The 3rd and final post of this series will cover the new Unwrap APIs, a Parallel namespace change, and some changes under the covers.

0 comments

Discussion is closed.

Feedback usabilla icon