C# Async Tips and Tricks, Part 3: Tasks and the Synchronization Context
TL;DR: It is possible to mix C# async and basic TPL style programming, but when doing so, the synchronization context capture feature of C# async is not forwarded to TPL continuations automatically, making UI dependent (and others) code fail and raise exceptions. This can lead to the termination of the process when exceptions are not handled properly, particularly in WinRT/C# apps.
I’ve discussed in a previous article of this series, the relation between async Task/Void methods and the ambient SynchronizationContext.
Just as a simple reminder, when executing a async method, whether it is Task, Task<T> or Void returning, the caller’s SynchronizationContext is captured to ensure that all the code in an async method is executed in the same context. The main scenario for this is to easily execute UI bound code in an async method.
It is important to remember that async methods are based on the TPL framework, and that async methods (except in infamous async void) return System.Threading.Tasks.Task instances.
[more]
This means that we can write something like this :
private static async Task SomeMethod() { var t = Task.Delay(TimeSpan.FromSeconds(1)) .ContinueWith( _ => Task.Delay(TimeSpan.FromSeconds(42)) ); await t; }
This is a perfectly valid code, which could arguably be written easier like this:
private static async Task SomeMethod() { await Task.Delay(TimeSpan.FromSeconds(1)); await Task.Delay(TimeSpan.FromSeconds(42)); }
It is possible to mix both TPL and C# async based code.
The SynchronizationContext and the TPL
But there’s yet another catch.
If both code blocks were identical, we could write something like this:
private async Task<string> SomeUIMethod() { var t = Task.Delay(TimeSpan.FromSeconds(1)) .ContinueWith( _ => this.Title = "Done !" ); return await t; }
When executed in a WPF like application, it fails with a CrossThreadAccessException or similar, where title cannot be set from the current thread.
There’s a simple explanation for this, being that the TPL does not use the SynchronizationContext like a C# async does, but rather uses a TaskScheduler.
With C# async methods, the underlying helper classes AsyncVoidMethodBuilder and AsyncTaskMethodBuilder both capture the Synchronization Context of the caller of an async method.
Simply put, when tasks are chained using the TPL methods like ContinueWith, the synchronization context of the caller is lost.
Capturing the synchronization context in TPL tasks
A TPL task scheduler is very similar to the SynchronizationContext, and it is so similar that there’s a helper method that allows adapting one to the other by using the TaskScheduler.FromCurrentSynchronizationContext() method.
It is possible to forward the SynchronizationContext to a chain of TPL tasks by specifying where the continuation's code should run, by using a special overload of ContinueWith that takes in a TaskScheduler:
private async Task SomeUIMethod() { var t = Task.Delay(TimeSpan.FromSeconds(1)) .ContinueWith( _ => this.Title = "Done !", // Specify where to execute the continuation TaskScheduler.FromCurrentSynchronizationContext() ); return await t; }
A bit tricky, isn’t it?
Why would I bother writing non C# async based code ?
You’re right, you probably should not bother and stick with async.
The problem though, is not that you should not bother; the problem is that you can write some. It’s very tempting to think that the two styles are equivalent, and that it will not matter which one is used.
Now, for the specific example of the thread-affinity of the UI, the issue is obvious. It crashes immediately.
But let’s consider the example of a custom SynchronizationContext that needs to handle exceptions a certain way. As long as you stay in “pure” C# async methods, exceptions will be raised in the proper synchronization context. But if there’s a native TPL method in the chain and an exception is raised, your process is terminated, because the exception is raised and unhandled in the ThreadPool.
The special case of Metro Windows Store apps
The problem goes a bit further, when in the context of the Metro Style apps.
Since Threads do not exist anymore in WinRT apps, the TPL in the WinRT world is forced to go with WinRT’s thread pool. In the case of the Task.Delay method, the promise (ContinueWith in this case) will be executed directly on the ThreadPool. If there’s no special TaskFactory and an exception is raised, then the exception will go unhandled directly into WinRT’s thread pool.
I have to admit though. This issue is a bit troubling, because it is very easy to crash the process, with any method that calls back from WinRT asynchronously. Any IAsyncOperation returning method, actually, which is a lot of them.
At the moment, I’ve yet to find where to intercept those exceptions since neither Application.Current.UnhandledException nor TaskScheduler.UnobservedTaskException are able to intercept it. (AppDomain.CurrentDomain.UnhandledException is not available in WinRT)
What to take away
If you decide to go the async way, try not to mix it up with the TPL, or at least not with Task.ContinueWith-like methods that may lose the current synchronization context unless you use the proper overload with a TaskScheduler. Worst case, you may end up terminating your process without realizing it.
As a side note, I've personally written a Static Analysis rule that prevents the use of the overload of ContinueWith that does not take a TaskScheduler, to avoid falling into this trap.