async/await does not “release the thread”
There is some language around async/await that I am going to stop using. I’ve heard others use it as well because it does help get the point across but I believe it is ultimately misleading. Async/await does not “release the thread.”
To see this you need to look at one level higher in your call stack than where you are awaiting. Consider the following code
1: private void Button_Click(object sender, RoutedEventArgs e)
2: {
3: LoadData();
4: }
5:
6: private async Task LoadData()
7: {
8: HttpClient client = new HttpClient();
9: string result1 = await client.GetStringAsync(new Uri("https://www.microsoft.com/)"));
10: int count = result1.Length;
11: string result2 = await client.GetStringAsync(new Uri("https://msdn.microsoft.com"));
12: count = result1.Length + result2.Length;
13:
14: MessageDialog messageDialog = new MessageDialog("Found " + count.ToString() + " characters");
15: await messageDialog.ShowAsync();
16: }
When people hear “release the thread” as in “when the code gets to the first ‘await’ on line 9, it releases the thread so that the UI can run while the async method happens” it gives the wrong impression that the thread is instantly put back into the thread pool (or something) and is available for arbitrary work to be assigned it.
The next thing it does is not arbitrary. When the execution gets to the await, the async operation is started and the thread returns to the immediate caller. In this case the caller is Button_Click which itself just returns. So no harm no foul right? Wrong. Lets make a minor change.
1: private void Button_Click(object sender, RoutedEventArgs e)
2: {
3: LoadData();
4: DoSomething();
5: DoSomethingElse();
6: }
7:
8: private async Task LoadData()
9: {
10: HttpClient client = new HttpClient();
11: string result1 = await client.GetStringAsync(new Uri("https://www.microsoft.com/)"));
12: int count = result1.Length;
13: string result2 = await client.GetStringAsync(new Uri("https://msdn.microsoft.com"));
14: count = result1.Length + result2.Length;
15:
16: MessageDialog messageDialog = new MessageDialog("Found " + count.ToString() + " characters");
17: await messageDialog.ShowAsync();
18: }
We haven't changed the method of LoadData, our async method. So it still works as planned. What we changed was the calling method. When the code gets to line 11 and hits the first await, the async operation is started and the thread returns to the caller. In this case we have added code after our call to our async method. That code will execute immediately on the same thread. So while GetStringAsync is executing, DoSomething will execute and then DoSomethingElse. Maybe that’s OK and maybe its not. Just don’t assume that the thread disappears and magically reappears later.
The thread doesn’t go away, it just returns and keeps on truckin’
AsyncAwaitNotReleaseThread.zip
Comments
- Anonymous
March 25, 2014
Sorry but I thought this was "obvious". - Anonymous
March 25, 2014
@Err to some it may be. But to many developers who don't work with threading or asynchronous methods on a regular basis there is a lot of "magic" that happens here and things don't go as they might expect. This post came out of a conversation I had yesterday with some developers who were new to the async/await model and were thinking that their entire call chain was going to be paused until the async op was completed. - Anonymous
March 25, 2014
The comment has been removed - Anonymous
March 26, 2014
Also worth noting that this code would give a warning that the first task isn't awaited. Pay attention to your warnings, or you'll need blog posts like this - Anonymous
March 27, 2014
I assume the fix would beawait LoadData(); - Anonymous
March 27, 2014
Yes, you get the warning if the method returns a Task (watch for green squiggles) but if your async method returns void then you get no warning at all. The general guidance is to only have an async method return void if it is an event handler which helps with that. Thanks! - Anonymous
March 27, 2014
@peter, yes that would be the fix if we wanted everything to run in series like traditional synchronous code (and that pushes the same issue to the previous caller in the stack). The bigger point is that you actually have a choice because the calling method gets control back when the asynchrony begins. You can await it, or you can take that returned Task and do a higher level of composition with other Tasks, or just ignore it if that is the appropriate behavior.