I'm learning about async/await ,and get confused on the explaination of the await of MSDN:"The await operator suspends execution until the work of the GetByteArrayAsync method is complete. In the meantime, control is returned to the caller of GetPageSizeAsync"
What i don't understand is,what does it mean "returned"? At first,i believed when a thread(let's say, the UI thread) reaches the "await" keyword, system will create a new thread(or get a thread from threadPool) to execute the rest of codes,and UI thread can return to the caller method and execute the rest.
But now i know that "await" will never create thread.
I write a demo:
class Program
{
static void Main(string[] args)
{
new Test().M1();
Console.WriteLine("STEP:8");
Console.Read();
}
}
class Test
{
public async void M1()
{
Console.WriteLine("STEP:1");
var t = M2();
Console.WriteLine("STEP:3");
await t;
}
public async Task<string> M2()
{
Console.WriteLine("STEP:2");
string rs = await M3();//when the thread reaches here,why don't it return to M1 and execute the STEP:3 ??
Console.WriteLine("STEP:7");
return rs;
}
public async Task<string> M3()
{
Console.WriteLine("STEP:4");
var rs = Task.Run<string>(() => {
Thread.Sleep(3000);//simulate some work that takes 3 seconds
Console.WriteLine("STEP:6");
return "foo";
});
Console.WriteLine("STEP:5");
return await rs;
}
}
this demo prints some labels representing the execute flow,which i thought it would be
STEP:1
STEP:2
STEP:3
STEP:4
STEP:5
STEP:6
STEP:7
STEP:8
(from 1 to 8 in order),
but the actual result is : Figure 1
STEP:1
STEP:2
STEP:4
STEP:5
STEP:3
STEP:8
STEP:6
STEP:7
If like MSDN's explaination, control is returned to M1 and print the "STEP 3", absolutely i'm wrong,So what's going on exactly?
Thanks in advance
Just to get it out of the way, there's no guarantee that, at any particular await
point we're going to do anything beyond carrying on with the rest of the code. However, in the sample in MSDN and your own example, we will always wait at each of the await
points.
So, we reach some point where we have await z
and we've decided that we're going to wait. This means a) That z
hasn't Completed yet (whatever it means for z
to be complete isn't something we care about at this moment in time) and b) That we have no useful work to do ourselves, at the moment.
Hopefully, from the above, you can see why "creating a new thread" or anything like it isn't necessary, because like I just said, there's no useful work to do.
Before we return control from our method though, we're going to enqueue a continuation. The async
machinery is able to express "when z has completed, arrange for the rest of this method to continue from our await
point".
Here, things like synchronization contexts and ConfigureAwait
become relevant. The thread that we're running on (and that we're about to relinquish control of) may be "special" in some way. It may be the UI thread. It may currently have exclusive access to some resources (think ASP.Net pre-Core Request/Response/Session objects).
If that's so, hopefully the system providing the special-ness has installed a synchronization context, and it's via that that we also are able to obtain "when we resume execution of this method, we need to have the same special circumstances we previously had". So e.g. we're able to resume running our method back on the UI thread.
By default, without a synchronization context, a thread pool thread will be found to run our continuation.
Note that, if a method contains multiple await
s that require waiting, after that first wait, we'll be running as a chained continuation. Our original caller got their context back the first time we await
ed.
In your sample, we have three await
points and all three of them will wait and cause us to relinquish control back to our caller (and we don't have multiple await
s in a single method to worry about either). All of these awaits are, at the end of the day, waiting for that Thread.Sleep
to finish (poor form in modern async
code which should use TaskDelay
instead). So any Console.WriteLine
s appearing after an await
in that method will be delayed.
However, M1
is async void
, something best avoided outside of event handlers. async void
methods are problematic because they offer no means of determining when they've completed.
Main
calls M1
which prints 1
and then calls M2
. M2
prints 2
and then calls M3
. M3
prints 4
, creates a new Task
, prints 5
and only then can it relinquish control. It's at this point that it creates a TaskCompletionSource
that will represent its eventual completion, obtain the Task
from it, and return that.
Notice that it's only when the call to M3
returns that we hit the await
in M2
. We also have to wait here so we do almost the same as M3
and return a Task
.
Now M1
finally has a Task
that it can await
on. But before it does that, it prints 3
. It returns control to Main
which now prints 8
.
Eons later, the Thread.Sleep
in rs
in M3
finishes running, and we print 6
. M3
marks its returned Task
as complete, has no more work to do, and so exits with no further continuations arranged.
M2
can now resume and its await
and print 6
. Having done that, its Task
is complete and M1
can finally resume and print 7
.
M1
doesn't have a Task
to mark as complete because it was async void
.
It's easiest to see when you look at what this code is actually doing under the hood. Here's an approximate translation of what that code is doing:
public void M1()
{
Console.WriteLine("STEP:1");
var t = M2();
Console.WriteLine("STEP:3");
t.ContinueWith(_ =>
{
//do nothing
});
}
public Task<string> M2()
{
Console.WriteLine("STEP:2");
Task<string> task = M3();
return task.ContinueWith(t =>
{
Console.WriteLine("STEP:7");
return t.Result;
});
}
public Task<string> M3()
{
Console.WriteLine("STEP:4");
var rs = Task.Run<string>(() =>
{
Thread.Sleep(3000);//simulate some work that takes 3 seconds
Console.WriteLine("STEP:6");
return "foo";
});
Console.WriteLine("STEP:5");
return rs.ContinueWith(t => t.Result);
}
Note that, for the sake of brevity, I've not done proper error handling here. One of the biggest benefits to using await
is that it handles errors in the way you generally want them to, and using the tools I have here...doesn't. This code also doesn't schedule the continuations to use the current synchronization context, something that's irrelevant to this situation, but is a useful feature.
At this point it hopefully is more clear as to what's going on. For starters, M1
and M3
really have no business being async
in the first place, as they never doing anything meaningful in their continuations. They should really just not be async
and to return the tasks they get from calling other methods. M2
is the only method that actually does anything useful in any of the continuations it creates.
It also makes it clearer as to the order of operations. We call M1
, which calls M2
, which calls M3
, which calls Task.Run
, then M3
returns a task that's identical to what Task.Run
returns and then returns itself, then M2
adds its continuation and returns, then M3
executes the next printline, then adds its continuation (which does nothing when it eventually runs), then returns. Then at some later point in time the Task.Run
finishes, and all of the continuations run, bottom up (because each is a continuation of the other).
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