I got into an interesting argument conversation with a co worker last week about whether async
/ await
was multi-threaded. He thought I was bonkers for suggesting it was not multi-threaded. So I did some research.
First off, obviously if you're doing async
/await
, it's probably because you want some multithreaded behavior like network IO or file IO where some other thread does some work for you while freeing your UI thread to handle UI stuff (or in the case of IIS, releasing your thread to handle other incoming requests, thus giving you better throughput). So my co-worker was right that 99% of the time, async
/await
will probably involve multiple threads.
However, if async
/await
were multi-threaded by its very nature, then it should be impossible to write a program using async
/await
that was single-threaded. So let's try to write a method that we can prove is single-threaded that also uses async
/await
. How about this:
public async void HandleClickEvent()
{
await Task.Yield();
j = 1;
while (j != 0)
{
if (j == -1) j++;
j++;
}
await Task.Yield();
}
It took some work to come up with an infinite loop that looked normal to the compiler, but that's what the while
loop is doing. If async
/await
were multi-threaded, then we might think that the UI thread would hit the first Task.Yield
and spawn off a new thread. Then the infinite loop would be run on a new thread and the UI would work great, right?
If we actually run that code in a Windows Store app the UI freezes. Why? Because, according to MSDN:
The async
and await
keywords don't cause additional threads to be created. Async
methods don't require multithreading because an async
method doesn't run on its own thread. The method runs on the current synchronization context and uses time on the thread only when the method is active. You can use Task.Run
to move CPU-bound work to a background thread, but a background thread doesn't help with a process that's just waiting for results to become available.
So when I claimed async
/await
wasn't multi-threaded, I was thinking of that. What's basically happening is that the UI thread is a message pump that processes events, and when you await within the UI thread's synchronization context, you yield control to the UI thread's message pump, which allows it to process UI events and such. When your awaited call returns, it throws an event back to the UI thread's message pump and the UI thread gets back to your method when it's done with anything else it's working on.
But after some research, I realized that I didn't know nearly enough about synchronization contexts and so I spent the morning reading about them. After a lot of research, I finally found someone that has a great description of how all this works under the covers and if you get the chance, I highly recommend reading C# MVP Jerome Laban's awesome series C# 5.0 Async Tips and Tricks.
In particular, one thing I learned is that if you start a new Task, you throw away the UI thread's synchronization context. If you await when there is no synchronization context, then by default WinRt
will give you some random thread from the thread pool, which may be different after each await
. In other words, if you do this:
public async Task RefreshAvailableAssignments()
{
await Task.Run(async () =>
{
Debug.WriteLine(Environment.CurrentManagedThreadId);
await Task.Yield();
Debug.WriteLine(Environment.CurrentManagedThreadId);
});
}
You will (usually) get a different thread after the yield
than you did before it. That can lead to trouble if you aren't careful and aren't aware of it. It can be especially dangerous if you're deep in the guts of something and you aren't 100% sure of whether you are being called from the UI thread or from some other thread. It can be particularly bad if someone after you decides to put your code into a Task.Run
and you were dependent upon the UI thread's synchronization context without being aware of it. Nasty, huh?
It makes me like more and more the idea introduced in the post by Jason Gorman entitled Can Restrictive Coding Standards Make Us More Productive? where he describes ways of discouraging team members from starting new threads (or Tasks on my project since WinRt doesn't give us Threads) unless there is a really good reason for doing so.
It goes back to a most excellent statement my co-worker made:
Async
/await
is very powerful, but we all know what comes with great power.
So that was fun. I look forward to having lots more constructive arguments conversations like this one in the future. :)