Async/await in Desktop Applications
This is a transcript of a demonstration I gave in our company internally about async/await “challenges” in UI programming. You can find the accompanying repository in my repository on Github.
The form contains three buttons which are intended to download the the content from http://microsoft.com asynchronously and show it in a TextBox.
Cross-Thread issue :(
private async void button1_Click(object sender, EventArgs e)
{
// await a completed task => will continue synchronously
await SomeFastAsyncOperation().ConfigureAwait(false);
// await a slow task => will continue in another thread
var t = await SomeSlowAsyncOperation().ConfigureAwait(false);
// write text to text box
textBox1.Text = t;
}
When clicking the first button, an InvalidOperationException
will
be thrown with the message
Cross-thread operation not valid: Control ‘textBox1’ accessed from a thread other than the thread it was created on.
That’s because in Win32 UIs you must access controls from the same thread
that created them. Because the after awaiting SomeSlowAsyncOperation
the method continues not in the main thread but a nackground thread,
accessing textBox1
is forbidden.
When you debug button1_Click
, pay attention to the Threads tool window.
- Entering the method, you’ll be on thread #1, the main thread.
- After calling
SomeFastAsyncOperation
, the code continues on thread #1. That’s because the method returns an already completed task, so the code can continue synchronously. - In contrast,
SomeSlowAsyncOperation
returns a not-completed task, therefore the succeeding code will continue not another one. (The UI thread will be released here and continue pumping the Win32 message queue) - The property
textBox1.Text
will be set in said background thread and fail, because Win32 controls mist be accessed from the same thread that created them.
Blocking :(
private async void button2_Click(object sender, EventArgs e)
{
// await a completed task => will continue synchronously
await SomeFastAsyncOperation().ConfigureAwait(false);
// await a slow task => will continue in another thread
var t = SomeSlowAsyncOperation().ConfigureAwait(false).GetAwaiter().GetResult();
// write text to text box
textBox1.Text = t;
}
Clicking the second button will freeze the application. The
GetAwaiter().GetResult()
invocation will try to re-enter the
the main thread, which is waiting for the task, so we’ll run into a dead-lock.
Run smoothly :)
private async void button3_Click(object sender, EventArgs e)
{
// force switch to threadpool thread
await TaskScheduler.Default;
// await a completed task => will continue synchronously
await SomeFastAsyncOperation().ConfigureAwait(false);
// await a slow task => will continue in another thread
var t = await SomeSlowAsyncOperation().ConfigureAwait(false);
// switch to main thread
await _joinableTaskFactory.SwitchToMainThreadAsync();
// write text to text box
textBox1.Text = t;
}
Using the JoinableTaskFactory
from Microsoft.VisualStudio.Threading
(NuGet package here),
we are able to “switch” back to the UI thread.
As the namespace implies, Microsoft.VisualStudio.Threading originates from the Visual Studio team. I stumbled over this library while reading the documentation for Visual Studio extensibility. Visual Studio is quite a complex application, and there are myriads of extensions available. To improve the start-up time, Microsoft strongly recommends to make use of asynchronous programming (see How to: Manage multiple threads in managed code and How to: Use AsyncPackage to load VSPackages in the background)
I won’t go into details of how async/await works. Basically, the compiler generates a state machine, which Dixin explains pretty good in his blog serie Understanding C# async / await (1) Compilation (Part 2, Part 3).
In our case, two calls are interesting:
await TaskScheduler.Default;
will continue the succeeding code in a threadpool thread.
(actually, the library provides an extension methodGetAwaiter(this TaskScheduler this)
. This works because the compiler uses a naming convention instead of requiring an interface implemenntation)await _joinableTaskFactory.SwitchToMainThreadAsync();
will continue the succeeding code in the main thread.
(actually, in the thread with instantiated_joinableTaskFactory
).
As you could see, Microsoft.VisualStudio.Threading makes asynchronous programming in desktop applications, both WinForms and WPF, much simpler.
BTW, I’ve learned a lot reading the code of that library. It provides
much more async helpers like AsyncEventHandlers
.
Here are some more links if you want to learn more about async programming:
- The 3 VS Threading Rules by Andrew Arnott
- Don’t Block on Async Code by Stephen Cleary
- Concurrency in C# Cookbook: Asynchronous, Parallel, and Multithreaded Programming by Stephen Cleary
Comments
Chris Weinert
I banged my head for hours trying to figure why our (100% working, boilerplate) console app code was not working in a WinForms app. Thank you for documenting this, and creating the GitHub repo as well! You saved my sanity :)
Swat
…spent hrs to figure out my perfect code :)
Saved the day… Thanks for the GitRepo!
Leave a Comment
Your email address will not be published. Required fields are marked *