This is part 2 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
Introduction to SynchronizationContext
An important part of the work in properly handling tasks on the server is supporting the synchronization context. When you’re using .NET 4.5, then the await
keyword automatically does this for you. When you’re consuming Task
objects on .NET 4, though, getting yourself back onto the right synchronization context is critical; otherwise, you may cause errors in your application when trying to access things which touch the HttpContext
in ASP.NET.
If you’re using ContinueWith
to provide a continuation to run when the task is finished, then you’ll want to stash the SynchronizationContext
and restore when it’s not null. The way you get back onto the right sync context is by calling Post
, which is itself an async method…but it’s an async method that returns void, not a Task
. The problem is, we’re going to want to return a task that doesn’t finish until everything is done running in the Post
callback. How can we do that without a Task
?
To be clear, there is nothing Task-specific about sync contexts. They've been with us since .NET 2.0, and if you ever did async/threaded work in ASP.NET (or anything else which has a sync context, like WinForms or WPF), you've always had to respect it. This is one the great things about await
in .NET 4.5: it means you can more or less just forget about this, because the compiler is writing all the boilerplate code that I'm about to show you.
TaskCompletionSource: Bridging non-Task async to Tasks
The TaskCompletionSource
class is an interesting beast. It’s an implementation of the task pattern whose task doesn’t mark itself as signaled until you call one of the methods that records whether the task is completed successfully, failed from an exception, or canceled.
As an example of how this works, let’s say you’re calling an "old school" .NET async API that uses the Async/Completed pattern (meaning, it uses events to signal when things are done), and you needed to convert this into a task. Your method might look like this:
public Task<int> SuperAccurateButSlowAdd(int x, int y) { asyncCalculator.Completed += (src, evt) => { int result = evt.Result; // But now what do I do with it? }; asyncCalculator.AddAsync(x, y); // And what do I return here? }
The solution to both of the problems above is to use TaskCompletionSource
.
public Task<int> SuperAccurateButSlowAdd(int x, int y) { var tcs = new TaskCompletionSource<int>(); asyncCalculator.Completed += (src, evt) => { int result = evt.Result; tcs.SetResult(result); }; asyncCalculator.AddAsync(x, y); return tcs.Task; }
One of the contracts for methods which return Task
or Task<T>
is that they should only throw exceptions if there was a problem with one of the parameters; otherwise, if they encounter an error in normal execution flow, then they should return a faulted task. So with error handling in both places that could throw, now our code looks like this:
public Task<int> SuperAccurateButSlowAdd(int x, int y) { var tcs = new TaskCompletionSource<int>(); try { asyncCalculator.Completed += (src, evt) => { try { int result = evt.Result; tcs.TrySetResult(result); } catch(Exception ex) { tcs.TrySetException(ex); } }; asyncCalculator.AddAsync(x, y); } catch (Exception ex) { tcs.TrySetException(ex); } return tcs.Task; }
Since you asked, yes, we do need both of those try/catch blocks. Although it's not clear at first glance, there are actually two functions in here: the main body of SuperAccurateButSlowAdd
, and the lambda (anonymous function) that we've wired up to the Completed
event. Of course, if you know neither one of those things can throw, then you can remove the try/catch, but you better be positive, because in some cases you could end up in a permanent waiting state.
If you're thinking this code is ripe for "helper methods", you're right. You're going to see that our Task
helper methods are really about reducing the code duplication to a single place, and wrapping things up with nice unit tests to boot.
Using TaskCompletionSource so we can call SynchronizationContext.Post
Now that we know how to adapt something which isn't Task
into something which is, this is what our code looks like when we want to do a ContinueWith
and get back on the proper sync context:
var ctxt = SynchronizationContext.Current; return SomeThingReturningATask() .ContinueWith(innerTask => { var tcs = new TaskCompletionSource<int>(); try { if (ctxt != null) { ctxt.Post(state => { try { int result = innerTask.Result; tcs.TrySetResult(result); } catch(Exception ex) { tcs.TrySetException(ex); } }, state: null); } else { int result = innerTask.Result; tcs.TrySetResult(result); } } catch(Exception ex) { tcs.TrySetException(ex); } return tcs.Task; });
Wow, look at all that duplication! :( And it's getting pretty hard to see our code for the noise. We're definitely going to want a helper here. And what's worse is that our code has a bug, and now it's hard to see what it is.
The next blog post will talk about what that missing code might be.