C# 5.0 Async Tips and Tricks, Part 1
Cet article est aussi disponible en francais.
TL;DR: This article is a discussion about how C#5.0 async captures the executing context when running an async method, which allows to easily stay on the UI Thread to access UI elements, but can be problematic when running CPU-bound work. Off of the UI thread, an async method may jump from thread to thread, breaking thread context dependent code along with it.
You can also read : Part 2, Async void
C# 5.0 has introduced, language-wise, just two of new features (async and caller information) and a gotcha fix (a lambda lifted foreach loop variable scope change).
The biggest addition though, in terms of compiler magic, is the addition async. It’s been covered all over the place, and I will just comment the addition feature by itself, and the impacts it has when using it.
This article is the first of a small series about some C# async tricks and gotchas that I will try to cover to give a bit of insight, and hopefully help developers avoid them.
[more]
C# 5.0 Async : The Objectives
Async programming has been a key concern over the past few years, originally to be able to have more fluid user interfaces. GUI on Windows are all using the concept of a UI Thread, that takes the form of a message pump that should always be processing messages. If that pump is stalled, because of some button click that is executing some blocking code, the UI freezes.
This is a very bad user experience, and Microsoft as well as others, have been either suggesting (or forcing, like in Silverlight or WinRT) developers to use asynchronous API calls.
When going asynchronous, using existing techniques like event completion messages or the Begin/End pattern, the biggest challenge is the keeping of state between each of these asynchronous calls. It requires the developer to make its own state machine to handle the passing of state between each stage of the asynchronous logical “operation”. This is quite a complex plumbing code, and having to write this every time is both time consuming and error prone.
From the developer’s point of view, C#’s async is to trying to make asynchronous code look synchronous. Asynchrony is a big problem in C#, and the C# team has introduced the feature to try to ease the life of developers when they have to deal with it.
Another objective of C#’s async is to be able to perform thread yielding, by explicitly releasing the control flow, therefore allowing a thread to be reused more effectively, by not actively waiting on some external processing. In some sense, it resembles to the concept of Win32 Fibers or more generally to concept of cooperative multitasking.
From my understanding, the ultimate goal is that over time, developers will favor writing asynchronous methods that will free up the UI thread when there is an asynchronous call involved.
C# 5.0 Async, Magic Level 1
When you start using it, it feels like it is doing a bit of “magic”, at first. Just put some async and await here and there, and it just works.
The first level of Magic happens in the state machine that is generated by the compiler the same way a developer would, and for the developer, this is almost transparent. An async method is then a series of chained delegates that are executed after each use of the await keyword.
Just taking a look at an ILSpy output shows all the guts of an async method, where a simple method is chunked into little pieces, driven by the presence of the await keyword.
You’ll find that all of your async method has been placed in a generated “Display Class”, and more specifically in a method called MoveNext.
While this code generation is more anecdotic than useful, it helps to understand the overall behavior of an async method.
The SyncronizationContext, Magic Level 2
If you’ve written some asynchronous code (without async), you must know about methods like Dispatcher.BeginInvoke or Form.BeginInvoke, that queue messages on the UI Thread. So, unless the framework does that for you (like with WebClient in Silverlight) you have to use these methods to update your UI when you asynchronous work has completed.
You must then wonder… How does an async method allow the update of the UI? That’s the Magic Level 2, known as the SynchronizationContext.
The synchronization context is basically an abstraction of a message queue, where you can synchronously or asynchronously queue up work, and get notified when it’s done. The Silverlight and WPF Dispatcher have one that is set automatically when executing UI related code. This simplification allows for an easy access to UI elements from an async method.
When creating an async method, the compiler relies on the AsyncTaskMethodBuilder, a BCL class that is used a coordinator to handle the magic behind the fact that when an async method is created, it captures the current synchronization context of a callee. It reuses that SynchronizationContext to queue up the execution of the code in the async method after the first await, so that it stays in the same context.
While this is internally a bit tricky, this is very effective when working on the UI Thread in an async method. There is almost no need to worry about going back on the UI Thread to update it, because the SynchronizationContext brought the execution flow back to it.
Asynchrony vs. Concurrency
Now that you’re executing your code in your async method, you may be tempted to think that no matter what you’ll do in your code, you won’t freeze the UI because you’re in an async method.
You’re going to be tempted to make a web call, then deserialize its content in your async method. You’ll soon find out that it freezes up your UI when the JSON/XML is deserialized.
That is one thing to remember about asynchrony. It’s not concurrency.
The tricky part where the SynchronizationContext brings you back on the UI Thread, brings you back exactly on it. Deserializing JSON is a CPU bound operation, which will block the thread it is working on, bringing you back to square one: your UI stutters.
At this point, you want concurrency, which is where async will not help you much. You need to execute your work on another thread, either via a Thread or a Task, or a Thread Pool item, … You’ll need to handle resources locking properly, graceful termination, all those nitty gritty details.
As soon as you leave the context of your UI bound async method, you need to handle communication with the UI by yourself, using Dispatcher.BeginInvoke and alike.
I personally think that it will be (and already is) one of the biggest misunderstandings of the use of async, where developers are going to think they’re doing parallel work using async, for which it is not the best fit.
Async Methods Life Cycle
Let’s consider this code :
private async void Button_Click_1(object sender, RoutedEventArgs e) { await Test(); var t = Task.Run(async () => await Test()); await t; } private async Task Test() { Debug.WriteLine("SyncContext : {0}", SynchronizationContext.Current); Debug.WriteLine("1. {0}", Thread.CurrentThread.ManagedThreadId); await Task.Delay(1000); Debug.WriteLine("2. {0}", Thread.CurrentThread.ManagedThreadId); await Task.Delay(1000); Debug.WriteLine("3. {0}", Thread.CurrentThread.ManagedThreadId); await Task.Delay(1000); Debug.WriteLine("4. {0}", Thread.CurrentThread.ManagedThreadId); }
Which, when executed produces this output :
SyncContext : System.Windows.Threading.DispatcherSynchronizationContext
1. 10
2. 10
3. 10
4. 10
SyncContext :
1. 6
2. 12
3. 12
4. 6
You’ll notice here is that the first execution of the method stays on the same Thread, the Dispatcher thread, using the DispatcherSynchronizationContext. This is why it is possible to execute UI bound code in this method.
In the case of WPF/Silverlight/Metro, the DispatcherSynchronizationContext is automatically set on a TLS (the Thread Static attribute) variable, where every single UI element callback will have access to it, and by extension, every async method executed in the same context.
But then, when the same method is executed inside of a manually started Task, the behavior is completely different. The code starts on the Thread 6, continues on the Thread 12, then back to 6.
The reason for this is the absence of SynchronizationContext when the async method was started. The default behavior of an async method, which is backed by AsyncTaskMethodBuilder, is to queue up work on the ThreadPool. By nature, it is not possible to choose which thread a queued work item is going to be executed on, hence the thread switching.
Do not assume that UI work can always be done in an async method, and that is it hazardous for code to rely on it.
Migrating Threading Context Dependent Code
Let’s consider this legacy code, which runs fine on WinForms 2.0 :
private void Button_Click_1(object sender, RoutedEventArgs e) { _lock.AcquireWriterLock(1000); // A context sensitive operation Thread.Sleep(3000); _lock.ReleaseWriterLock(); }
This is not the kind of code you’d want to see behind a UI event handler, but the use of a ReaderWriterLock could definitely be found in some background service, so bear with me.
Let’s say that you need to have you code migrated to WinRT, and that instead of the Thread.Sleep, you need to call an async method to do some file access:
private async void Button_Click_2(object sender, RoutedEventArgs e) { _lock.AcquireWriterLock(1000); // Do some context sensitive stuff... await Task.Delay(1000); _lock.ReleaseWriterLock(); }
Everything will run properly.
But you'll see that your UI freezes, so you want to put your code in a background Task:
private void Button_Click_3(object sender, RoutedEventArgs e) { Task.Run( async () => { try { _lock.AcquireWriterLock(1000); // Do some context sensitive stuff... await Task.Delay(1000); _lock.ReleaseWriterLock(); } catch (Exception ex) { Debug.WriteLine(ex); } } ); }
(Note: We'll come back on async lambdas, so move along :) )
That code will almost certainly break, probably nine times out of ten.
The reason for this is that the async code is run in a background thread, and that the code after the await will almost certainly not run on the same Thread as the code before the await.
But you may be lucky and run on the same thread. Your code may work. Sometimes.
That’s the reason why the lock keyword cannot be used in an async method, because the compiler will not ensure that the method will always be executed on the same thread, as it depends on the caller of the method.
What to take away
- Take a look at the generated code behind your async method, it’s very instructive.
- An async method is not guaranteed to run on the UI thread, it is using the ambient SynchronizationContext.
- Beware of thread dependent code executed in your async method, as the current thread may change throughout your method, depending on the ambient SynchronizationContext.
Stay tuned for the next part of this small series !