Processing Sequences of Asynchronous Operations with Tasks

Processing Sequences of Asynchronous Operations with Tasks

Rate This
  • Comments 6

Of late, I’ve seen multiple folks asking about how to use tasks to asynchronously execute a sequence of operations.  For example, given three synchronous functions:

public string DoA(string input);
public string DoB(string aResult);
public string DoC(string bResult);

you could invoke these functions with code like:

string aResult = DoA(input);
string bResult = DoB(aResult);
string cResult = DoC(bResult);

Then, given Task-based asynchronous counterparts to these functions:

public static Task<string> DoAAsync(string input);
public static Task<string> DoBAsync(string aResult);
public static Task<string> DoCAsync(string bResult);

how would you asynchronously do the equivalent of the synchronous code previously shown?

Async CTP and Language Support

The Async CTP highlights a great way to handle this once C# and Visual Basic have built-in support for awaiting tasks.  With that language support, the asynchronous version looks almost identical to the synchronous, albeit with a few extra keywords thrown in:

string aResult = await DoAAsync(input);
string bResult = await DoBAsync(aResult);
string cResult = await DoCAsync(bResult);

Lovely! Of course, .NET 4 doesn’t currently support that special syntax out-of-the-box, so what can we do in the meantime?  It’s important to realize that the Async CTP just builds on top of what’s available in .NET 4, so while the compiler here is doing what compilers do best and writing lots of boilerplate so that you don’t have to, the generated code is really still just using existing support in the Task Parallel Library.  We can use that support directly, too.

ContinueWith and Unwrap

One approach is to just use what’s provided in TPL without utilizing any additional helpers.  The Task.ContinueWith method schedules code to run when the antecedent task completes, and returns a Task (or Task<TResult>) to represent that subsequent operation.  So, we can try to handle the first chained call with code like the following:

var aResult = DoAAsync(input);
var bResult = aResult.ContinueWith(t => DoBAsync(t.Result));

This isn’t quite right, however.  This overload of ContinueWith creates a Task<TResult>, but the TResult here will be typed as the result of DoBAsync, which is Task<string>.  Thus, the instance returned from the ContinueWith call will be a Task<Task<string>>, which is not what we want.  To handle those nested tasks, we can utilize the Unwrap method included in TPL to “unwrap” the inner nested task, converting the Task<Task<string>> into the Task<string> we desire.  And with that small change, we can now complete our example:

var aResult = DoAAsync(input);
var bResult = aResult.ContinueWith(t => DoBAsync(t.Result)).Unwrap();
var cResult = bResult.ContinueWith(t => DoCAsync(t.Result)).Unwrap();

This approach works, and it may be all you need, but it does have a couple of downsides.  First, what happens if DoAAsync fails such that the returned task is faulted rather than running to completion?  In that case, the continuation off of aResult will still execute, and accessing the t.Result inside that function will propagate the exception.  That will cause bResult to be faulted, and its continuation will fire, with that continuation’s t.Result propagating the exception, and so on.  Net net, cResult will appropriately be faulted, but more work will have happened than was necessary (e.g. we invoked the continuations for B and C when we didn’t have to, and we threw several more exceptions than we needed to), and each time the exception was propagated it was wrapped in another level of AggregateException, so cResult will contain an AggregateException containing an AggregateException containing… and so on (AggregateException’s Flatten method is great for reducing these extra levels of aggregation, by the way).  Second, it’s a bit more code than you may otherwise like to write.  Can we do better?

Then

One of the really nice things about Tasks is that they enable good composition.  Once you have a single type capable of representing any arbitrary asynchronous operation, you can write “combinators” over the type that allow you to combine/compose asynchronous operations in a myriad of ways.  For example, we can abstract away this sequential combination notion into a Then combinator, and at the same time take care of the previously mentioned exception handling concerns. Our goal here will be to build the following operator:

public static Task<T2> Then<T1, T2>(this Task<T1> first, Func<T1, Task<T2>> next);

With that in place, we can then use it to re-implement our desired functionality with code like:

var aResult = DoAAsync(input);
var bResult = aResult.Then(s => DoBAsync(s));
var cResult = bResult.Then(s => DoCAsync(s));

The behavior of “Then” is that when the first task completes, the next function is invoked to produce a task and is provided with the output from the previous task.  The task returned from Then primarily represents the task returned by the next function’s invocation; however, if the first task is canceled or faults, the next function will not be invoked, and the returned task will instead represent the first task.  With this approach, we can chain Then calls together as was done in the above example, and the final task produced from the chain will represent all of the processing.

To implement Then, we first need the scaffolding to do argument validation and to create the task that Then will return:

public static Task<T2> Then<T1, T2>(this Task<T1> first, Func<T1, Task<T2>> next)
{
    if (first == null) throw new ArgumentNullException("first");
    if (next == null) throw new ArgumentNullException("next");

    var tcs = new TaskCompletionSource<T2>();
    … // TODO #1: implement Then logic
    return tcs.Task;
}

This code validates that the source task and the function to produce the subsequent task are both non-null.  It then creates a TaskCompletionSource<TResult> which is used to return a Task from Then; the rest of our implementation will be all about completing that TCS task with the appropriate state.  Now we just need to fill in that TODO #1:

// in place of TODO #1 above
first.ContinueWith(delegate
{
    if (first.IsFaulted) tcs.TrySetException(first.Exception.InnerExceptions); 
    else if (first.IsCanceled) tcs.TrySetCanceled();
    else
    {
        … // TODO #2: handle successful completion of first
    }, TaskContinuationOptions.ExecuteSynchronously);
}

We’re utilizing a continuation off of the first task to run some code when the task completes.  If it completes due to faulting, we transfer its exceptions to the TCS task we’re returning from Then, and we’re done.  Similarly, if the first task was canceled, we cancel the TCS task.  That just leaves a RanToCompletion final state, meaning that the first task completed successfully, and we’ll handle that case in a moment.  The other thing to notice is that we’ve specified that this continuation task should ExecuteSynchronously: that just means that, if possible, this continuation function will run synchronously with regards to the first task completing, ideally happening on the same thread immediately after first completes, rather than TPL scheduling the task to execute later.  Now let’s complete our implementation by finishing TODO #2:

// in place of TODO #2 above
try
{
    var t = next(first.Result);
    if (t == null) tcs.TrySetCanceled();
    else t.ContinueWith(delegate
    {
        if (t.IsFaulted) tcs.TrySetException(t.Exception.InnerExceptions);
        else if (t.IsCanceled) tcs.TrySetCanceled();
        else tcs.TrySetResult(t.Result);
    }, TaskContinuationOptions.ExecuteSynchronously);
}
catch (Exception exc) { tcs.TrySetException(exc); }

Here we invoke the next function with the result of the first task.  If for some reason next returns null, we’ve opted to cancel the TCS task, but this could be changed to fault it or to do something else entirely, based on your desired semantics.  Assuming a non-null Task is returned, we then continue from that task.  This is done to enable transferring of the results from the that task to the TCS task since, as mentioned, the TCS task returned from Then should be a proxy for this inner task object.  The rest should look familiar, simply transferring the final state and associated data from this inner task to the TCS task.  Here’s our final implementation of this method:

public static Task<T2> Then<T1, T2>(this Task<T1> first, Func<T1, Task<T2>> next)
{
    if (first == null) throw new ArgumentNullException("first");
    if (next == null) throw new ArgumentNullException("next");

    var tcs = new TaskCompletionSource<T2>();
    first.ContinueWith(delegate
    {
        if (first.IsFaulted) tcs.TrySetException(first.Exception.InnerExceptions);
        else if (first.IsCanceled) tcs.TrySetCanceled();
        else
        {
            try
            {
                var t = next(first.Result);
                if (t == null) tcs.TrySetCanceled();
                else t.ContinueWith(delegate
                {
                    if (t.IsFaulted) tcs.TrySetException(t.Exception.InnerExceptions);
                    else if (t.IsCanceled) tcs.TrySetCanceled();
                    else tcs.TrySetResult(t.Result);
                }, TaskContinuationOptions.ExecuteSynchronously);
            }
            catch (Exception exc) { tcs.TrySetException(exc); }
        }
    }, TaskContinuationOptions.ExecuteSynchronously);
    return tcs.Task;
}

There are of course many variations on this possible, and you could imagine building up several overloads of Then to handle a multitude of cases for creating chains of asynchronous processing, e.g.

public static Task Then(this Task first, Action next);
public static Task Then(this Task first, Func<Task> next);
public static Task<T2> Then<T2>(this Task first, Func<T2> next);
public static Task<T2> Then<T2>(this Task first, Func<Task<T2>> next);

public static Task Then<T1>(this Task<T1> first, Action<T1> next);
public static Task Then<T1>(this Task<T1> first, Func<T1,Task> next);
public static Task<T2> Then<T1,T2>(this Task<T1> first, Func<T1,T2> next);
public static Task<T2> Then<T1,T2>(this Task<T1> first, Func<T1, Task<T2>> next);

Sequence

With such methods, we can create additional abstractions for sequential processing as well.  Let’s say we did have an implementation for an additional one of the above overloads:

public static Task Then(this Task first, Func<Task> next)
{
    if (first == null) throw new ArgumentNullException("first");
    if (next == null) throw new ArgumentNullException("next");

    var tcs = new TaskCompletionSource<object>();
    first.ContinueWith(delegate
    {
        if (first.IsFaulted) tcs.TrySetException(first.Exception.InnerExceptions);
        else if (first.IsCanceled) tcs.TrySetCanceled();
        else
        {
            try
            {
                var t = next();
                if (t == null) tcs.TrySetCanceled();
                else t.ContinueWith(delegate
                {
                    if (t.IsFaulted) tcs.TrySetException(t.Exception.InnerExceptions);
                    else if (t.IsCanceled) tcs.TrySetCanceled();
                    else tcs.TrySetResult(null);
                }, TaskContinuationOptions.ExecuteSynchronously);
            }
            catch (Exception exc) { tcs.TrySetException(exc); }
        }
    }, TaskContinuationOptions.ExecuteSynchronously);
    return tcs.Task;
}

We can layer a Sequence method on top of this as follows:

public static Task Sequence(params Func<Task> [] actions)
{
    Task last = null;
    foreach (var action in actions)
    {
last = (last == null) ? Task.Factory.StartNew(action).Unwrap() : last.Then(action);
    }
    return last;
}

and with such a Sequence method, we’re now able to write a set of lambdas that will be processed asynchronously one after the other, e.g.

Task<string> aResult = null, bResult = null, cResult = null;
Sequence(
    () => { return aResult = DoAAsync(input); },
    () => { return bResult = DoBAsync(aResult.Result); },
    () => { return cResult = DoCAsync(bResult.Result); });

Iterate

We can build other interesting combinators for sequential processing, including one that takes advantage of C# iterators.  There are several popular frameworks that use C# iterators to drive asynchronous processing, and the same trick employed in those can be used with tasks.  Here’s a basic example of such a driver:

public static Task Iterate(IEnumerable<Task> asyncIterator)
{
    if (asyncIterator == null) throw new ArgumentNullException("asyncIterator");

    var enumerator = asyncIterator.GetEnumerator();
    if (enumerator == null) throw new InvalidOperationException("Invalid enumerable - GetEnumerator returned null");

    var tcs = new TaskCompletionSource<object>();
    tcs.Task.ContinueWith(_ => enumerator.Dispose(), TaskContinuationOptions.ExecuteSynchronously);

    Action<Task> recursiveBody = null;
    recursiveBody = delegate {
        try {
            if (enumerator.MoveNext()) enumerator.Current.ContinueWith(recursiveBody, TaskContinuationOptions.ExecuteSynchronously);
            else tcs.TrySetResult(null);
        }
        catch (Exception exc) { tcs.TrySetException(exc); }
    };

    recursiveBody(null);
    return tcs.Task;
}

This Iterate method accepts an enumerable of tasks.  This enumerable needs to be lazy, such that the next task isn’t generated until MoveNext is called to retrieve it; this is exactly the behavior we’ll generally get with a C# iterator (and a VB iterator in Async CTP, but if you have the Async CTP, you should just use await as called out at the beginning of this post).  The Iterate method retrieves an enumerator and begins by calling MoveNext on it.  The task returned from the enumerator as Current then has a continuation hooked up to it which, when the retrieved task completes, starts that process again, calling MoveNext on the enumerator, hooking up a continuation, and so on.  Only once MoveNext throws an exception or returns false does this process end.  With such an Iterate method in place, we can now rewrite our original sample with a method like this:

IEnumerable<Task> DoExample(string input)
{
    var aResult = DoAAsync(input);
    yield return aResult;
    var bResult = DoBAsync(aResult.Result);
    yield return bResult;
    var cResult = DoCAsync(bResult.Result);
    yield return cResult;
    …
}

which we then invoke with code like:

Task t = Iterate(DoExample(“42”));

There are of course many variations on the Iterate implementation you could easily add.  For example, you might want the iteration to stop if a retrieved task ends in the faulted state; that can be accomplished with some minor modifications to the recursiveBody function.

LINQ

Getting slightly more wacky (but still within the realm of reasonable), we can utilize the C# and VB LINQ syntax to also express sequential processing.  The C# and VB compilers are pattern-based in their support of the .NET standard query operators, allowing you to use from, select, and so on with arbitrary data types as long as methods matching the right signature are exposed.  We can, for example, implement an appropriate SelectMany operator as an extensions method on top of Task<TResult>:

static class Extensions
{
    public static Task<TResult> SelectMany<TSource, TCollection, TResult>(
        this Task<TSource> source,
        Func<TSource, Task<TCollection>> collectionSelector,
        Func<TSource, TCollection, TResult> resultSelector)
    {
        if (source == null) throw new ArgumentNullException("source");
        if (collectionSelector == null) throw new ArgumentNullException("collectionSelector");
        if (resultSelector == null) throw new ArgumentNullException("resultSelector");

        return source.ContinueWith(t =>
        {
            return collectionSelector(t.Result).
                ContinueWith(c => resultSelector(t.Result, c.Result), TaskContinuationOptions.NotOnCanceled);
        }, TaskContinuationOptions.NotOnCanceled).Unwrap();
    }
}

and with that, we can then re-implement our original example with code like:

var result = from aResult in DoAAsync("42")
             from bResult in DoBAsync(aResult)
             from cResult in DoCAsync(bResult)
             select cResult;

An implementation of a bunch of the LINQ operators targeting tasks is available as part of the Parallel Extensions Extras project at http://code.msdn.microsoft.com/ParExtSamples.  If instead we were to utilize the Reactive Extensions available for download from the DevLabs site, we can utilize the ToObservable extension method it provides for Task<TResult>, and then utilize Rx’s robust LINQ implementation to do the same thing, e.g.

var result = from aResult in DoAAsync("42").ToObservable()
             from bResult in DoBAsync(aResult).ToObservable()
             from cResult in DoCAsync(bResult).ToObservable()
             select cResult;

F# Async Workflows

Out-of-the-box in Visual Studio 2010, F# supports asynchronous workflows and has built-in capability for awaiting tasks.  This allows you to write the original example with code like:

let example = async {
    let! aResult = Async.AwaitTask DoAAsync()
    let! bResult = Async.AwaitTask DoBAsync()
    let! cResult = Async.AwaitTask DoCAsync()
    …
}

Summary

As you can see, there are many useful and interesting ways to achieve sequential, asynchronous processing using Tasks.  Eventually when built-in language support is available for awaiting tasks, that will be the easiest and the recommended approach for developing asynchronous code of this nature.  In the meantime, you can try out such support in the Async CTP, and you can use other techniques as described in this blog post to achieve similar kinds of processing today, assured that you’ll be able to update that code in the future with the more powerful language capabilities when they’re available.

Happy coding!

Leave a Comment
  • Please add 5 and 2 and type the answer here:
  • Post
  • Great post !!!

  • Hi,

    great post (as always)!

    For the initial example - or more general for sequential, asynchronous processing - why not just a naive version like

    Task.Factory.StartNew(() =>

    {

       string aResult = DoA(input);

       string bResult = DoB(aResult);

       string cResult = DoC(bResult);

    });

    ?

    For me with this the same sequential, asynchronous processing is achieved*. Instead of giving single operations to the task scheduler here's a batch scheduled. But is there anything I didn't consider? ContinueWith is super, but for this (simple) example the usage isn't obvious for me.

    * let's assume only cResult is of final interest

    Thanks in advance!

    Regards

  • This is great stuff - I am exactly in need of what you are describing, having used the Async Ctp and wishing I had a rtm version of it. I am sure I'll be able to handle it with the pointers in your post. Thanks again!

    @Anton: You're missing the point of asynchronous calls, you're code is synchronous - you only offloaded it to a thread pool thread. This is good and well if you're doing a computation, but if any of the DoX() methods hits a database, for example, you're thread is blocked dead and waiting for a IO operation to complete. It can't return and handle some other work.

  • Anton,

    I think one of the issues with taking the naive approach as you described is what happens if DoA for instance performs long running IO work and blocks the calling thread unless invoked asynchronously. By having DoA return a Task instance followed up by continuations for invoking the subsequent methods, you're able to create a pattern in which long running work invoked by those methods return a task that can be used without blocking the calling thread while work is being done.

    - James

  • This is exactly what I was looking for... Great post!

  • Excellent stuff, just what I was looking for.

Page 1 of 1 (6 items)