I understand that async which awaits a task yields execution back to the caller, allowing it to continue until it needs the result.
My interpretation of what I thought would come out of this was correct up until a certain point. It looks as though there's some sort of interleaving going on. I expected Do3() to complete and then back up the call stack to Do2(). See the results.
await this.Go();
Which calls
async Task Go()
{
await Do1(async () => await Do2("Foo"));
Debug.WriteLine("Completed async work");
}
async Task Do1(Func<Task> doFunc)
{
Debug.WriteLine("Start Do1");
var t = Do2("Bar");
await doFunc();
await t;
}
async Task Do2(string id)
{
Debug.WriteLine("Start Do2: " + id);
await Task.Yield();
await Do3(id);
Debug.WriteLine("End Do2: " + id);
}
async Task Do3(string id)
{
Debug.WriteLine("Start Do3: " + id);
await Task.Yield();
Debug.WriteLine("End Do3: " + id); // I did not expect Do2 to execute here once the method call for Do3() ended
}
The expected outcome:
// Start Do1
// Start Do2: Bar
// Start Do2: Foo
// Start Do3: Bar
// Start Do3: Foo
// End Do3: Bar
// End Do2: Bar
// End Do3: Foo
// End Do2: Foo
//Completed async work
Actual output:
//Start Do1
//Start Do2: Bar
//Start Do2: Foo
//Start Do3: Bar
//Start Do3: Foo
//End Do3: Bar
//End Do3: Foo
//End Do2: Bar
//End Do2: Foo
//Completed async work
What's going on here exactly?
I'm using .NET 4.5 and a simple WPF app to test my code.
This is a WPF app, and all of this code is executed on the same UI thread. Each await
continuation in your code is scheduled via DispatcherSynchronizationContext.Post
, which posts a special Windows message to the UI thread's message queue. Each continuation happens in the order its message was posted (this is implementation-specific and you should not rely upon this, but that's how it works here).
So, the continuation for End Do3: Foo
is indeed posted right after the one for End Do3: Bar
. The output is correct.
Now, a bit more details. When I asked about WinForms vs WPF, I expected your "expected" output to match the actual output. I just tested it under WinForms, and it does match:
// Start Do1
// Start Do2: Bar
// Start Do2: Foo
// Start Do3: Bar
// Start Do3: Foo
// End Do3: Bar
// End Do2: Bar
// End Do3: Foo
// End Do2: Foo
//Completed async work
So, why the discrepancy between WPF and WinForms, while both run a message loop, and we only deal with single-threaded code here? The answer can be found here:
Why a unique synchronization context for each Dispatcher.BeginInvoke callback?
WPF's DispatcherSynchronizationContext.Post
just calls Dispatcher.BeginInvoke
, and one substantial WPF's implementation detail is that each Dispatcher.BeginInvoke
callback is executed on its own unique synchronization context, as explained in the linked question.
This affects the await
continuations for Task
objects (like await doFunc()
). In WinForms, such continuations are inlined (executed synchronously), because SynchronizationContext.Current
stays the same. In WPF, they're not inlined but rather posted via SynchronizationContext.Post
, because SynchronizationContext.Current
before await task
and after completion of the task
is not the same (it gets compared inside task.GetAwaiter().OnCompleted
by the await
runtime infrastructure code).
Thus, in WPF, it's often the same UI thread, but different synchronization context associated with this thread, so the continuation may incur one more asynchronous PostMessage
callback to be posted to, pumped and executed by the message loop. Besides YieldAwaitable
(returned by Task.Yield
), you'd also experience this behavior for TaskCompletionSource.SetResult
-style continuations triggered from a WPF UI thread.
This is rather complex but implementation-specific stuff. If you want to have precise control over the order of asynchronous await
continuations, you may want to roll out your own synchronization context, similar to Stephen Toub's AsyncPump
. Although usually you don't need that, especially for a UI thread.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With