This is part 4 in a series on using Task Parallel Library when writing server applications, especially ASP.NET MVC and ASP.NET Web API applications.
- Introduction
- SynchronizationContext
- ContinueWith
- TaskHelpers
Simple Task Helpers
I know we left off in part 3 with a problem, but we're not going to solve it yet, because we still need to build some foundation helper classes that will make it easier to do that. In particular, there are some good practices we can codify around the creation and conversion of tasks that led us to write the TaskHelpers class that we use today in ASP.NET Web Stack.
Caching Tasks (and Disposability)
One of the important things to remember about TaskCompletionSource
is that it can be somewhat expensive to create. If you look at the implementation of the async
/await
compiler feature, you'll see that they go to great lengths to cache a bunch of reusable tasks; we don't go quite as far, but we have at least centralized our creation of completed, canceled, and faulted tasks so that we can improve this caching logic as time goes on, as we see fit.
You might've noticed that the Task
class implements IDisposable
; doesn't that mean you should dispose it when you're done with it? Wouldn't that invalidate all this caching work? It turns out the answer is not so simple; the truth is that it's often impossible to know who the true owner of a task is. Stephen Toub, the architect behind the Task Parallel Library, argues that you should not dispose your Task
objects, and his work with the compiler team to enable their caching behavior in .NET 4.5 is the ultimate validation of this argument. Resist the temptation: do not dispose your Task
!
Canceled Tasks
The first and easiest helpers to show are for the creation of canceled tasks. These are also the easiest thing to cache, since they have no state, so we also get to show how caching might work for the more complex versions.
public static class TaskHelpers { public static Task Canceled() { return CancelCache<AsyncVoid>.Canceled; } public static Task<TResult> Canceled<TResult>() { return CancelCache<TResult>.Canceled; } } static class CancelCache<TResult> { public static readonly Task<TResult> Canceled = GetCancelledTask(); static Task<TResult> GetCancelledTask() { var tcs = new TaskCompletionSource<TResult>(); tcs.SetCanceled(); return tcs.Task; } } struct AsyncVoid { }
We are taking advantage of the static initialization facilities of the C# language, combined with generics, to get a create-on-demand, cached-for-app-lifetime canceled task.
You'll notice that I'm using a private struct (AsyncVoid
) as a stand-in to represent the T
for our cancel cache. We could have just as easily used any other type, but my feeling here is that it should be trivial to look at some Task
object which is-a Task<T>
and know whether it was purposefully or accidentally casted down to Task
. Perhaps just as importantly, there is no non-generic version of TaskCompletionSource
, so not having the result be a Task<T>
was kind of out of the question.
Completed Tasks
Our next task creation helper is around completed tasks. Here, we do a small amount of caching for completed Task
, as well as a completed Task<object>
which returns null. The compiler in .NET 4.5 is much more aggresive about pre-computing and caching completed tasks with common return values; we could do the same thing at some point in the future if we so desired, since we funnel all our helpers through here.
public static class TaskHelpers { static readonly Task<object> _completedTaskReturningNull = FromResult<object>(null); static readonly Task _defaultCompleted = FromResult<AsyncVoid>(default(AsyncVoid)); public static Task Completed() { return _defaultCompleted; } public static Task<TResult> FromResult<TResult>( TResult result) { var tcs = new TaskCompletionSource<TResult>(); tcs.SetResult(result); return tcs.Task; } public static Task<object> NullResult() { return _completedTaskReturningNull; } }
Faulted Tasks
The final kind of task creation we need to support is faulted tasks. There is really no opportunity for caching here, since the exception objects will be different every time. Note that there are two versions here, one which takes a single exception and one which takes many exceptions; the latter is useful when you're aggregating the results of several faulted tasks together.
public static class TaskHelpers { public static Task FromError( Exception exception) { return FromError<AsyncVoid>(exception); } public static Task<TResult> FromError<TResult>( Exception exception) { var tcs = new TaskCompletionSource<TResult>(); tcs.SetException(exception); return tcs.Task; } public static Task FromErrors( IEnumerable<Exception> exceptions) { return FromErrors<AsyncVoid>(exceptions); } public static Task<TResult> FromErrors<TResult>( IEnumerable<Exception> exceptions) { var tcs = new TaskCompletionSource<TResult>(); tcs.SetException(exceptions); return tcs.Task; } }
Run Synchronously
Calling Task.Factory.StartNew
when you don't actually need to do anything asynchronously can be expensively wasteful, because it's likely to spin up a worker thread and cause a thread switch to run your inherently synchronously code. We have several overloads of RunSynchronously
which can be used to get Task
objects which are already completed and contain the result of the synchronous code. These functions also accept cancellation tokens, and return canceled tasks when the token is signaled; they also take advantage of our previous helper methods to get caching benefits.
public static class TaskHelpers { public static Task RunSynchronously( Action action, CancellationToken token = default(CancellationToken)) { if (token.IsCancellationRequested) return Canceled(); try { action(); return Completed(); } catch (Exception e) { return FromError(e); } } public static Task<TResult> RunSynchronously<TResult>( Func<TResult> func, CancellationToken token = default(CancellationToken)) { if (token.IsCancellationRequested) return Canceled<TResult>(); try { return FromResult(func()); } catch (Exception e) { return FromError<TResult>(e); } } public static Task<TResult> RunSynchronously<TResult>( Func<Task<TResult>> func, CancellationToken token = default(CancellationToken)) { if (token.IsCancellationRequested) return Canceled<TResult>(); try { return func(); } catch (Exception e) { return FromError<TResult>(e); } } }
Copying Task Results to TaskCompletionSource
The final bit of helpers for this post are a few extension methods for TaskCompletionSource
which can be used to partially or fully copy the results of a task onto the completion source. These helpers only set the results if the task is currently complete; they don't wait for the task to complete if it has not already done so.
public static class TaskHelpers { public static bool TrySetFromTask<TResult>( this TaskCompletionSource<TResult> tcs, Task source) { if (source.Status == TaskStatus.Canceled) return tcs.TrySetCanceled(); if (source.Status == TaskStatus.Faulted) return tcs.TrySetException( source.Exception.InnerExceptions); if (source.Status == TaskStatus.RanToCompletion) { var tr = source as Task<TResult>; return tcs.TrySetResult( tr == null ? default(TResult) : tr.Result); } return false; } public static bool TrySetFromTask<TResult>( this TaskCompletionSource<Task<TResult>> tcs, Task source) { if (source.Status == TaskStatus.Canceled) return tcs.TrySetCanceled(); if (source.Status == TaskStatus.Faulted) return tcs.TrySetException( source.Exception.InnerExceptions); if (source.Status == TaskStatus.RanToCompletion) { // Sometimes the source is Task<Task<TResult>>, // and sometimes it's Task<TResult>. Handle both. var ttr = source as Task<Task<TResult>>; if (ttr != null) return tcs.TrySetResult(ttr.Result); var tr = source as Task<TResult>; if (tr != null) return tcs.TrySetResult(tr); return tcs.TrySetResult( FromResult(default(TResult))); } return false; } public static bool TrySetIfFailed<TResult>( this TaskCompletionSource<TResult> tcs, Task source) { switch (source.Status) { case TaskStatus.Canceled: case TaskStatus.Faulted: return tcs.TrySetFromTask(source); } return false; } }
What's Next?
We've got a firm understanding of the fundamentals, and built up all the necessary infrastructure, so now we're ready to start writing our replacement helpers for the existing Task methods; in particular, we will be looking to completely replace the usage of ContinueWith
with safer, targeted, a better performing helper methods.
ContinueWith has a TaskContinuationOptions.ExecuteSynchronously flag which can be set.
I know you want to remove ContinueWith completely and for that you need to create your own version of ExecuteSynchronously code. But for those who stick to ContinueWith this is a good alternative.
Posted by: softlion | May 09, 2012 at 03:45
It would be great to see continuation of this posts series with applying of helpers.
Posted by: Account Deleted | June 22, 2012 at 06:55
Agree with Mikhail, that it would be nice if this thread was tied up.
Posted by: Gregory | July 09, 2012 at 08:33
I will definitely be tying it up shortly. :)
Posted by: Brad Wilson | July 09, 2012 at 08:43
TaskContinuationOptions.ExecuteSynchronously does not guarantee that the continuation will be executed synchronously. We have learned that the hard way.
More info here: http://blogs.msdn.com/b/pfxteam/archive/2012/02/07/10265067.aspx
Posted by: Ligaz | July 12, 2012 at 23:44
Brad, I feel so uncomfortable with caching TaskCompletionSource. HttpResponseMessage (on the .Result) is not an immutable object as such state of my cached object can change through the pipeline as such re-using there is no guarantee. So my cached object will be different (e.g. status code can change) for different requests let alone concurrent requests.
If HttpResponseMessage was immutable then that would work but now I would pay the penalty of creating TCS rather than caching and fear that something might change it.
Posted by: Ali | July 20, 2012 at 06:32
I agree that you should only cache TaskCompetionSource where the value is immutable. The compiler in VS2012 will also do something of this quietly for you when returning common constant values (like 0).
Posted by: Brad Wilson | July 20, 2012 at 11:37
tying this up soon?
Posted by: livingston | April 11, 2013 at 21:00
Sorry about that. I put it on my TODO list. It got lost in my departure from Microsoft.
Posted by: Brad Wilson | April 12, 2013 at 18:21