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

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

  • Comments 10

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.

Leave a Comment
  • Please add 8 and 4 and type the answer here:
  • Post
  • Would you be able to send tasks to be attached other tasts?

    Somthing like:

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

    {

               DoWork();

    });

    Task c = Task.Factory.StartNewWithParent((p) =>

    {

     DoWork();

    });

    p.Wait();

  • Gal,

    No, a child Task may only be created from within another Task's context. The reason is that Attached Tasks (and the parent/child relationships that result) are intended for use in structured parallelism scenarios, where the scope of parallel code is clearly defined. Allowing Tasks to attach randomly to other Tasks outside of that context sort of defeats that purpose.

    Thanks,

    Danny

  • Gal, in that scenario, i think its better to use the WaitAny/WaitAll methods:

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

     DoWork( );

    } );

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

     DoWork( );

    } );

    Task.WaitAll( p, c );

  • i would like to have an overload of ContinueWhenAny that takes an IEnumerable<Task> because often you create a dynamic number of tasks. At least make it IList<Task> which has the same functionality but can be used with a List<Task>.

  • Hi Tobi,

    We actually thought of doing this but couldn't find enough compelling reasons. For your scenario (dynamic number of Tasks), you can still use containers like List<> or Queue<> with the existing APIs. You just have to call ToArray.

    Thanks,

    Danny

  • i agree with tobi (and have commented about that before to)

    i cant see any compelling reason NOT to use ienumerable.. you add more power and remove none :)

    what if your array of task is infinite? what if you want to create new tasks until some condition that the already created tasks affect? what if creating the tasks takes a long time?(perhaps it includes a webservice call or something) what if you create 1000 tasks and the first one complete immediattly, and you where using WaitAny?

    when all of .net is moving to more lazy eval, these two methods do the opposite.. :)

    yes, there are problems, mostly with WaitAll, but for WaitAny its much easier

    imo you should really reconsider this..

  • aL and tobi,

    Point taken =). I should be clear: we couldn't find enough compelling reasons *given where we were in the cycle*. No one concluded that having IEnumerable<> support was a bad idea. However, no scenarios are really lost; things are just somewhat more inconvenient. And I believe we can still do this for future releases.

    Also, I just realized that my sense of deja vu was due to the discussions following this post (in which both of you participated):

    http://blogs.msdn.com/pfxteam/archive/2009/04/14/9549246.aspx

  • Hiya

    Why is the task state constructors of type of Object? Could it not be

    generic of type T?

    e.g. Rather than:

     public Task( Action<Object> action, Object state)

    Could it be:

     public Task( Action<T> action, T state)

    And also:

     public Task( Func<Object, TResult> function, Object state)

    To:

     public Task( Func<T, TResult> function, T state)

    Sorry if syntax is off...

    This is a syntax that seems to be used by the ContinueWith methods as

    they expect a Func or Action that takes as Task as the state object

    e.g.

     ContinueWith(TResult)(Func(Task, TResult))

     ContinueWith(TNewResult)(Func(Task(TResult), TNewResult))

    However this does not seem to be exposed in the regular Task and

    Task(TResult) constructors

    Thank you for your consideration

  • Hey Jeremy,

    Task implements the IAsyncResult interface, which has an "Object AsyncState {get;}" property. This is the reason we work with Objects and force you to cast the state.

    Regarding ContinueWith, the parameter passed to the delegate is the Task (or Task<TResult>) that you are continuing off of, not a user-supplied state. In fact, we don't currently support state with ContinueWith.

    Thanks for the comment,

    Danny

  • While trying to come up with a compelling example; I have realised that closures/anonymous functions could be used to pass strongly typed state outside IAsyncResult use.

    [Ignoring that it can also be "passed" by calling a non-shared member to have access to object state]

    Also I think the Pipeline example in the  ParallelExtensionsExtras CoordinationDataStructures provides a better route to what I was looking at - so thank you for that too :-)

Page 1 of 1 (10 items)