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.
Hello Brad and thanks for your interesting posts.
You mentioned writing unit-tests for TPL. I'm curious how do you unit-test something that happens on multiple threads? I read "Growing OO software Guided by Tests" from Freeman and Pryce, they wrote about unit-testing concurrent apps, but still, I'm not sure I got the point.
Posted by: Account Deleted | April 11, 2012 at 07:53
Nice Brad. But I need to learn '</int>' in C#(which is visible in your code, lol).
Posted by: Imran Baloch | April 11, 2012 at 23:32
The source code highlighting tool does not like HTML-looking things in <pre> blocks, even though it's entirely legal HTML. :-p
Posted by: Brad Wilson | April 12, 2012 at 05:50
Using a unit testing framework which supports Task<T> (like xUnit.net :)) helps. I'll cover some of the unit testing stuff later in the series.
Posted by: Brad Wilson | April 12, 2012 at 05:53
Is there any reason not to use FromCurrentSynchronizationContext method? :)
Posted by: Soe Moe Tun Lwin | April 16, 2012 at 00:28
It throws when you don't have a synchronization context.
Posted by: Brad Wilson | April 16, 2012 at 07:32
Yeah i get this with syntax highlughter all the time and have to html encode before pasting.
Posted by: Aliostad | April 29, 2012 at 01:15
Hi,
Can you please let me know how to stash the SynchronizationContext and restore when it’s not null.I am trying to make a database call and in the ContinueWith method i want to use Response object to flush data as cvs file to the UI.
Posted by: Hari | May 02, 2012 at 13:29
Within an ASP.Net worker thread I can access HttpContext.Current but SynchronizationContext.Current is null.
Something I'm doing wrong?
Posted by: David | May 25, 2012 at 09:25
What does "ASP.NET worker thread" mean in this context? How is this different from the request thread?
HttpContext.Current is settable, but that doesn't necessarily mean that it was set by ASP.NET.
Posted by: Brad Wilson | May 29, 2012 at 08:56
The final code sample above does exactly what you want.
Posted by: Brad Wilson | May 29, 2012 at 08:57