Can the last await be replaced with an explicit wait?
-
11-12-2019 - |
Question
I'm still learning the async
/await
, so please excuse me if I'm asking something obvious. Consider the following example:
class Program {
static void Main(string[] args) {
var result = FooAsync().Result;
Console.WriteLine(result);
}
static async Task<int> FooAsync() {
var t1 = Method1Async();
var t2 = Method2Async();
var result1 = await t1;
var result2 = await t2;
return result1 + result2;
}
static Task<int> Method1Async() {
return Task.Run(
() => {
Thread.Sleep(1000);
return 11;
}
);
}
static Task<int> Method2Async() {
return Task.Run(
() => {
Thread.Sleep(1000);
return 22;
}
);
}
}
This behaves as expected and prints "33" in the console.
If I replace the second await
with an explicit wait...
static async Task<int> FooAsync() {
var t1 = Method1Async();
var t2 = Method2Async();
var result1 = await t1;
var result2 = t2.Result;
return result1 + result2;
}
...I seem to get the same behavior.
Are these two examples completely equivalent?
And if they are equivalent in this case, are there any other cases where replacing the last await
by an explicit wait would make a difference?
Solution 3
OK, I think I figured this out so let me sum it up, in what will hopefully be a more complete explanation than the answers provided so far...
Short Answer
Replacing the second await
with an explicit wait will have no appreciable effect on a console application, but will block the UI thread of a WPF or WinForms application for the duration of the wait.
Also, the exception handling is slightly different (as noted by Stephen Cleary).
Long Answer
In a nutshell, the await
does this:
- If the awaited task has already finished, it just retrieves its result and continues.
- If it hasn't, it posts the continuation (the rest of the method after the
await
) to the current synchronization context, if there is one. Essentially,await
is trying to return us where we started from.- If there isn't a current context, it just uses the original TaskScheduler, which is usually thread pool.
The second (and third and so on...) await
does the same.
Since the console applications typically have no synchronization context, continuations will typically be handled by the thread pool, so there is no issue if we block within the continuation.
WinForms or WPF, on the other hand, have synchronization context implemented on top of their message loop. Therefore, await
executed on a UI thread will (eventually) execute its continuation on the UI thread as well. If we happen to block in the continuation, it will block the message loop and make the UI non-responsive until we unblock. OTOH, if we just await
, it will neatly post continuations to be eventually executed on the UI thread, without ever blocking the UI thread.
In the following WinForms form, containing one button and one label, using await
keeps the UI responsive at all times (note the async
in front of the click handler):
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
private async void button1_Click(object sender, EventArgs e) {
var result = await FooAsync();
label1.Text = result.ToString();
}
static async Task<int> FooAsync() {
var t1 = Method1Async();
var t2 = Method2Async();
var result1 = await t1;
var result2 = await t2;
return result1 + result2;
}
static Task<int> Method1Async() {
return Task.Run(
() => {
Thread.Sleep(3000);
return 11;
}
);
}
static Task<int> Method2Async() {
return Task.Run(
() => {
Thread.Sleep(5000);
return 22;
}
);
}
}
If we replaced the second await
in FooAsync
with t2.Result
, it would continue to be responsive for about 3 seconds after the button click, and then freeze for about 2 seconds:
- The continuation after the first
await
will politely wait its turn to be scheduled on the UI thread, which would happen afterMethod1Async()
task finishes, i.e. after about 3 seconds, - at which point the
t2.Result
will rudely block the UI thread until theMethod2Async()
task finishes, about 2 seconds later.
If we removed the async
in front of the button1_Click
and replaced its await
with FooAsync().Result
it would deadlock:
- The UI thread would wait on
FooAsync()
task to finish, - which would wait on its continuation to finish,
- which would wait on the UI thread to become available,
- which it isn't, since it is blocked by the
FooAsync().Result
.
The article "Await, SynchronizationContext, and Console Apps" by Stephen Toub was invaluable to me in understanding this subject.
OTHER TIPS
Your replacement version blocks the calling thread waiting for the task to finish. It's hard to see a visible difference in a console app like that since you're intentionally blocking in Main, but they're definitely not equivalent.
They are not equivalent.
Task.Result
blocks until the result is available. As I explain on my blog, this can cause deadlocks if you have an async
context that requires exclusive access (e.g., a UI or ASP.NET app).
Also, Task.Result
will wrap any exceptions in AggregateException
, so error handling is harder if you synchronously block.