Where does async/await execute awaitable code(in our example downloading a web site) cause control yields to the next row of code of our program and program just asks result of Task getStringTask? We know that no new threads, no thread pool are not used.
If the operation is truly asynchronous, then there's no code to "execute". You can think of it as all being handled via callbacks; the HTTP request is sent (synchronously) and then the HttpClient
registers a callback that will complete the Task<string>
. When the download completes, the callback is invoked, completing the task. It's a bit more complex than this, but that's the general idea.
I have a blog post that goes into more detail on how asynchronous operations can be threadless.
Am I right in my silly assumption that CLR just switches the current executable code and awaitable part of the method between each other in scope of one thread?
That's a partially true mental model, but it's incomplete. For one thing, when an async
method resumes, its (former) call stack is not resumed along with it. So async
/await
are very different than fibers or co-routines, even though they can be used to accomplish similar things.
Instead of thinking of await
as "switch to other code", think of it as "return an incomplete task". If the calling method also calls await
, then it also returns an incomplete task, etc. Eventually, you'll either return an incomplete task to a framework (e.g., ASP.NET MVC/WebAPI/SignalR, or a unit test runner); or you'll have an async void
method (e.g., UI event handler).
While the operation is in progress, you end up with a "stack" of task objects. Not a real stack, just a dependency tree. Each async
method is represented by a task instance, and they're all waiting for that asynchronous operation to complete.
Where is continuation of awaitable part of method performed?
When awaiting a task, await
will - by default - resume its async
method on a captured context. This context is SynchronizationContext.Current
unless it is null
, in which case it is TaskScheduler.Current
. In practice, this means that an async
method running on a UI thread will resume on that UI thread; an async
method handling an ASP.NET request will resume handling that same ASP.NET request (possibly on a different thread); and in most other cases the async
method will resume on a thread pool thread.
In the example code for your question, GetStringAsync
will return an incomplete task. When the download completes, that task will complete. So, when AccessTheWebAsync
calls await
on that download task, (assuming the download hasn't already finished) it will capture its current context and then return an incomplete task from AccessTheWebAsync
.
When the download task completes, the continuation of AccessTheWebAsync
will be scheduled to that context (UI thread, ASP.NET request, thread pool, ...), and it will extract the Length
of the result while executing in that context. When the AccessTheWebAsync
method returns, it sets the result of the task previously returned from AccessTheWebAsync
. This in turn will resume the next method, etc.