7

I am really curious how async/await enables your program not to be halted. I really like the way how Stephen Cleary explains async/await: "I like to think of "await" as an "asynchronous wait". That is to say, the async method pauses until the awaitable is complete(so it waits), but the actual thread is not blocked (so it's asynchornous)."

I've read that async method works synchronously till compilator meets await keywords. Well. If compilator cannot figure out awaitable, then compilator queues the awaitable and yield control to the method that called method AccessTheWebAsync. OK. Inside the caller (the event handler in this example), the processing pattern continues. The caller might do other work that doesn't depend on the result from AccessTheWebAsync before awaiting that result, or the caller might await immediately. The event handler is waiting for AccessTheWebAsync, and AccessTheWebAsync is waiting for GetStringAsync. Let's see an msdn example:

async Task<int> AccessTheWebAsync()
{ 
    // You need to add a reference to System.Net.Http to declare client.
    HttpClient client = new HttpClient();

    // GetStringAsync returns a Task<string>. That means that when you await the 
    // task you'll get a string (urlContents).
    Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");

    // You can do work here that doesn't rely on the string from GetStringAsync.
    DoIndependentWork();

    // The await operator suspends AccessTheWebAsync. 
    //  - AccessTheWebAsync can't continue until getStringTask is complete. 
    //  - Meanwhile, control returns to the caller of AccessTheWebAsync. 
    //  - Control resumes here when getStringTask is complete.  
    //  - The await operator then retrieves the string result from getStringTask. 
    string urlContents = await getStringTask;

    // The return statement specifies an integer result. 
    // Any methods that are awaiting AccessTheWebAsync retrieve the length value. 
    return urlContents.Length;
}

Another article from msdn blog says that async/await does not create new thread or use other threads from thread pool. OK.

My questions:

  1. 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<string> getStringTask? We know that no new threads, no thread pool are not used.

  2. 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? But changing the order of addends does not change the sum and UI might be blocked for some unnoticeable time.

i3arnon
  • 113,022
  • 33
  • 324
  • 344
StepUp
  • 36,391
  • 15
  • 88
  • 148

3 Answers3

15

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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
2

In general the continuation (the part of your method after await) can run anywhere. In practice it tends to run on the UI thread (e.g. in a Windows application) or on the thread pool (e.g. in an ASP .NET server). It can also run synchronously on the caller thread in some cases ... really it depends on what kind of API you're calling and what synchronization context is being used.

The blog article you linked does not say that continuations are not run on thread pool threads, it merely says that marking a method as async does not magically cause invocations of the method to run on a separate thread or on the thread pool.

That is, they're just trying to tell you that if you have a method void Foo() { Console.WriteLine(); }, changing that to async Task Foo() { Console.WriteLine(); } doesn't suddenly cause an invocation of Foo(); to behave any differently at all – it'll still be executed synchronously.

Joren
  • 14,472
  • 3
  • 50
  • 54
  • So if awaitable code tries to run at UI thread, it can halt UI. However, halts of UI are not occurring if async code is used. – StepUp Jul 02 '15 at 14:49
  • @StepUp The asynchronous call doesn't run on the UI thread. The continuation can (if this is desirable, for instance updating a text box with the result of your HTTP call). From your question title, that's what you're asking right – where does the *continuation* run? – Joren Jul 02 '15 at 14:52
  • I am really stacked, what's difference between continuation and asynchronous code? Cause continuation and asynchronous code will be tasked if it is not possible to return immediately result of awaitable code – StepUp Jul 02 '15 at 14:54
  • @StepUp The continuation comes after the asynchronous operation. It is the code after the `await`. In your case it's `return urlContents.Length;`. It, of course, needs a thread to run on and it's usually a `ThreadPool` thread. But the actual asynchronous operation doesn't need one. – i3arnon Jul 02 '15 at 15:27
2

If by "awaitable code" you mean the actual asynchronous operation, then you need to realize that it "executes" outside of the CPU so there's no thread needed and no code to run.

For example when you download a web page, most of the operation happens when your server sends and receives data from the web server. There's no code to execute while this happens. That's the reason you can "take over" the thread and do other stuff (other CPU operations) before awaiting the Task to get the actual result.

So to your questions:

  1. It "executes" outside of the CPU (so it's not really executed). That could mean the network driver, a remote server, etc. (mostly I/O).

  2. No. Truly asynchronous operations don't need to be executed by the CLR. They are only started and completed in the future.


A simple example is Task.Delay which creates a task that completes after an interval:

var delay = Task.Delay(TimeSpan.FromSeconds(30));
// do stuff
await delay;

Task.Delay internally creates and sets a System.Threading.Timer that will execute a callback after the interval and complete the task. System.Threading.Timer doesn't need a thread, it uses the system clock. So you have "awaitable code" that "executes" for 30 seconds but nothing actually happens in that time. The operation started and will complete 30 seconds in the future.

i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • Thanks, but what about if code awaits Task result where executes, for example, number of Fibonacci? It is an operation which requires CPU activity, not just waiting of downloaded result. – StepUp Jul 02 '15 at 14:46
  • 2
    "it executes outside of the CPU so there's no thread needed and no code to run." - This can in general be true for the asynchronous operation, but is **not** true for the continuation, which I think is what was being asked. The continuation *will* run on the CPU and *will* run on a thread. – Joren Jul 02 '15 at 14:50
  • @Joren I don't think that's what's being asked. – i3arnon Jul 02 '15 at 15:21
  • 1
    @StepUp A CPU bound operation isn't naturally asynchronous. It can be offloaded to a `ThreadPool` thread (i.e. with `Task.Run`) and be treated as if it was, but that's not the general case for async-await. – i3arnon Jul 02 '15 at 15:23
  • 1
    @StepUp In that case the operation **does need** a thread, which is a `ThreadPool` thread – i3arnon Jul 02 '15 at 15:24
  • Thank you very much @i3arnon – StepUp Jul 03 '15 at 02:40
  • @i3arnon: Yeah, I didn't release OP wasn't entirely on the same page when it comes to terminology. – Joren Jul 03 '15 at 07:54