I get in a lot of fights.
async/await anyway. Despite async/await being over 5 years old (AOTW), there still remains a lot of confusion regarding some basic functionality.
I do not presume to be an in-depth expert. Some of the things I read from Stephen Cleary or Jon Skeet make my eyes go crossed. But for your average, everyday usage, it doesn’t need to be so misunderstood.
I am not going to rehash the excellent articles that explain these things. I am going to share a WPF application that demonstrates some of the commonly misunderstood pitfalls so you can actually observe the things you read about by the experts like Stephen Cleary, Stephen Toub, Jon Skeet.
Introducing “WPFGui”…. This is a simple Windows Presentation Foundation application that will attempt to update the UI on each of the button clicks. Of course I don’t need to update the UI directly with MVVM, but this is a contrived example after all….
This application provides six different uses of
async/await in a GUI application. As you probably know, only the UI thread is allowed to update the user interface. Async programming can both promote responsiveness if used correctly, and destroy it if not.
As you run this application keep an eye on the “Thread ID” value. Some of the operations will briefly update this field depending on the state of the SynchronizationContext . It is visual evidence of what is going on under the hood.
Save Cross Context (will crash) (SaveCrossContext.cs)
The first button is labeled “Save Cross Context (will crash)”. Take a guess what happens when you press it. The reason for the crash is that an async method is called which is configured to run “context-unaware”. So, during the execution of this code, the context switches from the “context-aware” to “context un-aware”. You should observe the Thread ID changing before the program crashes. After awaiting, the context is no longer using the UI thread. A call to
UpdateDescription() tries to set the Text property of a text box and crashes because : “The calling thread cannot access this object because a different thread owns it.”
This is different than a deadlock, which we will see soon.
Save Context Aware (SaveContextAware.cs)
This time, there is no switching context because
ConfigureAwait(false) is not applied to the await. The Thread ID never changes. All continuations resume on the original captured context. Since that is the same context owning the UI thread we have no problem updating the UI. However, be aware that this can present performance issues if there is some intensive work going on.
Save Split Cross Context (SaveSplitCrossContext.cs)
This example demonstrates how to do the bulk of the work in a new context, but resume on the original context, thus enabling UI access, by only using
ConfigureAwait inside the method that is called, not on the actual call. When you execute this option observe the Thread ID. It will briefly change, then change again back to the original value. The first switch happens when the code enters
CallSomethingAsync() and then again after it finishes awaiting, where execution resumes on the original captured context. Use this technique to offload as much work as possible but still resume on the original context.
In my experience, this is a very common mistake. You have a method that is not async, but you want to use an async API. You find a cornucopia of advice on stackoverflow.com to simply add
.Result to the method call. The problem with this is that, while your call synchronously blocks (yes, synchronously, it is not aysnc at all!) the method you called is awaiting the context to be available. Classic deadlock.
No Deadlock (SaveWithNoDeadlock.cs)
ConfigureAwait(false) to the rescue! We have avoided the deadlock by running the called code in a different context. Fantastic! Obviously we can block on async API’s all we want because
ConfigureAwait(false). That’s another 15 reputation points on stackoverflow for me! Except…..
ConfigureAwait False doesn’t always save you! (SaveAndDeadLockEvenWithConfigureAwaitFalse.cs)
ConfigureAwait(false) is just a hack is because any code in the call chain might not use
ConfigureAwait(false) to appropriately execute context-unaware async code. On the surface, the code behind this button looks just like the previous example that did NOT deadlock. It uses
.Result, and applies
ConfigureAwait(false) within the
CallSomethingAsync method. However, this time it calls a library method that is itself
async and the programmer who wrote it was a bit rude –
ConfigureAwait(false) is not applied within that library. So, when the method runs, the context being awaited is actually the same context being blocked by
.Result. DEADLOCK. Even if the library is your own code it can be easy for someone maintaining the system to introduce this mistake in the future.
Incidentally, while it is impossible to make the mistake because it won’t compile, be aware that you cannot
ConfigureAwait a call that is not awaited!
Hopefully seeing these simple examples and watching them run helps you understand what you can and cannot (or should not) do with
async code. I have a very simple rule, if my code base is not async compatible I use synchronous APIs, period.