This is the third part of the Async Wandering series. For your convenience you can find other parts in the table of contents in Part 1 – Why creating Form from WinForms in unit tests breaks async?
async
and await
are great features of C#. By using them we can hide all cumbersome details of callbacks and write code in “synchronous” way. However, without good understanding of internals we might quickly run into troubles. Especially if we want to introduce asynchronous methods in synchronous context. Let’s see.
Table of Contents
Challenge
Imagine that we have synchronous method in WinForms codebase. Actual technology is not that important, in WPF you might quickly get the same problem since it uses similar concepts.
Now imagine, that you want to run some lengthy action and you would like to have it asynchronous, however, before ending your job you would like to wait for the results. Snippet representing the idea:
1 2 3 4 5 6 7 8 9 10 11 |
public void SynchronousAction(){ // Doing stuff var task = LengthyAction(this.Form); // Doing even more stuff task.Wait(); // We want to make sure that action is done return; } public async Task LengthyAction(Form form){ // Crazy stuff going on here } |
SynchronousAction
runs on UI thread, LengthyAction
might run somewhere else. This solution might be sufficient for our needs (if lengthy action is not that lengthy or else we will freeze the UI), however, imagine now, that action does the following:
1 2 3 |
public async Task LengthyAction(Form form){ form.Invoke(someLambda); } |
This quickly leads to a deadlock.
Deadlock?
Invoke
works by posting message to other thread’s message loop and waits until it is handled. In our case, UI thread cannot process the message because it is blocked on task.Wait()
method. If we would use await
, then we would have no such problem — await in WinForms awaits for tasks on message pump and is able of processing messages. This is very important detail which you don’t need to know as long as you do not block UI thread explicitly. General rule of thumb is: if you use async
somewhere in your codebase, you must use it all the way up. Every method must be asynchronous or else you need to be very careful with waits.
Ok, so how can we solve this problem without rewriting everything to asynchronous methods? Well, we can do some dirty magic.
Running message pump manually
Instead of waiting for a task indefinitely, we will perform busy waiting and run message loop by hand:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public void SynchronousAction(){ { // Doing stuff var signal = new ManualResetEvent(false ); var task = Task.Run( async () => { await LengthyAction(this.Form); signal.Set(); }); // Doing even more stuff while (!signal.WaitOne( TimeSpan.FromMilliseconds(1))) { Application.DoEvents(); } return; } |
Instead of running the method directly, we create another task with asynchronous lambda. This new task awaits for lengthy action and sets signal when it is done. We are guaranteed that the second task will not set the signal too early, because we can simply await for our lengthy action.
Next, we cannot block UI thread. Instead of waiting indefinitely, we sleep for a millisecond, call message pump and retry the operation. We will not move forward until the signal is set, however, if lengthy action posts a message to our thread, we will execute it with a little delay (one millisecond here).
Summary
Does it work? Yes. Is it nice? Well, we might argue here. Introducing asynchronous code in synchronous codebase is not easy, we cannot simply replace all methods with asynchronous ones because sometimes it is technically impossible. How do you await for asynchronous event handlers? And if you know (since it is possible but cumbersome), how do you do that in VB.NET (in that language event cannot return anything, so you don’t have a task to wait for). This snippet might save your life one day, but you had better don’t need to try.
Bonus chatter: WPF doesn’t have a Application.DoEvents()
for running message pump, however, it looks like it is possible to achieve similar effect with this code.