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

Danny Shih

Related posts:

Last time, we covered Tasks being detached by default and some refactorings in our multiple-Task continuation APIs.  The final post of this series will discuss Nested Tasks and Unwrap, a Parallel namespace change, and some changes under the covers.

Nested Tasks and Unwrap

We’ve added the Unwrap APIs to address scenarios that deal with nested Tasks.  Before jumping into Unwrap, let’s first talk about nested Tasks, e.g. a Task<Task> or Task<Task<TResult>>.  For example:

var nestedTask = Task.Factory.StartNew(() =>
{
    return Task.Factory.StartNew(() =>
    {
        return 42;
    });
});

Nested Tasks commonly lead to unexpected behavior in applications.  For example, consider an API that provides the following functionality for asynchronously logging into a web service (like one from Facebook), retrieving a list of friends, and sending an email to each friend.

// Given a user name and password, returns a Task whose

// result is a UserToken object.

public Task<UserToken> LogOn(string username, string password);

 

// Given a UserToken, returns a Task whose result

// is a collection of Friend objects.

public Task<FriendCollection> GetFriendList(UserToken userToken);

 

// Given a Friend, subject, and body, returns a Task that

// represents an email sending operation.

public Task SendEmail(Friend friend, string subject, string body);

 

A user would like to be able to write code like the following, utilizing these APIs and Task continuations:

var userToken = LogOn(user, pass);

var friends = userToken.ContinueWith(_ => GetFriendList(userToken.Result));

var emails = friends.ContinueWith(_ =>

{

    var sentMails = new List<Task>();

    foreach(var friend in friends.Result)

    {

        sentMails.Add(SendEmail(friend, subject, body));

    }

    return Task.Factory.ContinueWhenAll(

        sentMails.ToArray(), tasks => Task.WaitAll(tasks));

});

emails.ContinueWith(_ => Console.WriteLine(“All emails sent”));

 

The bolded code is problematic.  Because the GetFriendList method returns a Task<FriendCollection>, the ‘friends’ variable is actually going to be a Task<Task<FriendCollection>>.  This will cause a compiler error at the foreach loop, because ‘friends.Result’ will return a Task<FriendCollection> instead of a FriendCollection.  The compiler error in this case is a good thing, of course, highlighting a programming error.  However, once a developer realizes the type mismatch, he still has to code additional logic to unwrap the ‘friends’ Task so that it returns an actual FriendCollection.  This logic is nontrivial, especially if it is to correctly deal with exceptions, cancellation, etc.  The last line is also problematic.  The emails variable here is actually of type Task<Task>.  The outer Task will complete once the inner Task is returned from its body, even if the inner Task hasn’t completed.  The net result of this is that “All emails sent” could be written out prior to all of the email tasks actually completing.

Now, you may have noticed that, in Beta 1, we provided special ContinueWith overloads to deal with this type of scenario.  For example:

public Task<TResult> ContinueWith<TResult>(

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

 

The Func returns a Task<TResult>, so normally, ContinueWith would return a Task<Task<TResult>>.  But this ContinueWith overload does some magic under the covers to return a Task<TResult>.  There were a number of reasons why we didn’t like this approach, including:

·         Too much magic.  It’s hard to explain why one set of ContinueWith overloads is “just different”.

·         Only works for ContinueWith.  What if user scenarios result in nested Tasks for other Task creation APIs like StartNew, ContinueWhenAll, etc.?

·         What if nested Tasks are actually desired?  If a user actually wants that Task<Task<TResult>>, he still might unknowingly bind to this magical overload.

Given these reasons, our solution for Beta 2 and beyond is two Unwrap extension methods.

namespace System.Threading.Tasks

{

    public static class TaskExtensions

    {

        public static Task Unwrap(

            this Task<Task> task);

        public static Task<TResult> Unwrap<TResult>(

            this Task<Task<TResult>> task);

    }

}

 

These methods may be used to transform any Task<Task> or Task<Task<TResult>> into a Task or Task<TResult>, respectively.  The transformation performed produces a Task or Task<TResult> that fully represents the original nested Task, including exceptions, cancellation state, etc. 

With Unwrap, we can fix the above scenario (note the bolded).

var userToken = LogOn(user, pass);

var friends = userToken.ContinueWith(

    _ => GetFriendList(userToken.Result)).Unwrap();

var emails = friends.ContinueWith(_ =>

{

    var sentMails = new List<Task>();

    foreach(var friend in friends.Result)

    {

        sentMails.Add(SendEmail(friend, subject, body));

    }

    return Task.Factory.ContinueWhenAll(

        sentMails.ToArray(), tasks => Task.WaitAll(tasks));

}).Unwrap();

emails.ContinueWith(_ => Console.WriteLine(“All emails sent”));

 

Parallel Namespace Change

We’ve moved the Parallel class from the System.Threading namespace to the System.Threading.Tasks namespace.  We found that most applications needed to bring in both namespaces when using TPL, so why not put everything into one namespace?  Additionally, Parallel is such a common word (and will likely become more so in the future), and System.Threading such a common namespace, we wanted to reduce the chances of conflict with other .NET types as much as possible.

Here’s a useful IDE tip.  Use “Ctrl + .” to automatically bring in the relevant namespace once you’ve typed a class/type name.

 Ctrl Period IDE

Under the Covers

Beta 2 contains quite a few bug fixes not done for Beta 1.  All of them were important, but we’ll focus on only two here.

First, Parallel.For and ForEach have been tweaked for better load-balancing with other workloads in the current or other AppDomains.  Essentially, the change was to service the parallel loops with Tasks that periodically retired and re-queued themselves, allowing other contenders to grab Threads and make progress.

Second, waiting for Tasks in parent/child relationships has become more efficient.  In Beta 1 and before, parent Tasks waited for their children using explicit Waits, so even if a parent completed first, it would burn a thread until all of its children completed.  In Beta 2, parent Tasks wait for their children using callbacks; the parent maintains a count for number of children it has, and each child decrements the count as it completes.  This waiting logic significantly improves scalability.

That’s it folks!  We hope you’ve enjoyed this series.  Download Beta 2 and try it out!

ctrl-period.png

0 comments

Discussion is closed.

Feedback usabilla icon