23

I've been reading about the new async await keyword and it sounds awesome, but there is one key question I haven't been able to find the answer for in any of the intro videos I've watched so far (I also read the whitepaper a while back).

Suppose I have a call to await in a nested function on the main UI thread. What happens to the thread at this point? Does control go back to the message loop and the UI thread is free to process other inputs?

When the awaited task completes, does the entire stack get pushed onto a message queue, such that control will return through each of those nested functions, or is something else entirely happening here?

And secondly (while I have your attention), I don't really understand why asynchronous methods need to be labeled with async. Can't any method be executed asynchronously? What if I want to execute a method asynchronously but it doesn't have an async keyword--is there a way to do that simply?

Cheers. :)

Edit: Admittedly if I could get the sample code compiling I could probably just figure that out myself, but for one reason or another I'm running into a block there. What I really want to know is to what extent does a continuation continue... does it freeze the entire call stack, to resume it when the task completes, or does it only go back so far? Does a function itself need to be marked as async in order to support continuation, or (as I asked originally) does it continue the entire call stack?

If it doesn't freeze the entire call stack, what happens when the async await hits a non-async calling function? Does it block there? Wouldn't that defeat the point of await? I hope you can see that I'm missing some understanding here that I hope someone can fill in so I can continue to learn this.


  • Eric Lippert has a series of blog posts on how await/async works. This should answer all of your questions. - dtb
  • Jon Skeet also has a series of posts where he walks through the Async CTP and basically re-implements it. He also shows the compiler-generated code for a couple scenarios. - bbogovich
  • Both links are no longer valid, Eric Lippert's blog post is now here and Eduasync is now here - dlatikay

2 답변


26

Suppose I have a call to await in a nested function on the main UI thread. What happens to the thread at this point? Does control go back to the message loop and the UI thread is free to process other inputs?

Yes. When you await an awaitable (such as a Task<TResult>), the thread's current position within the async method is captured. It then queues the remainder of the method (the "continuation") to be executed when the awaitable is finished (e.g., when a Task<TResult> completes).

However, there is an optimization that can take place: if the awaitable is already finished, then await doesn't have to wait, and it just continues executing the method immediately. This is called the "fast path", described here.

When the awaited task completes, does the entire stack get pushed onto a message queue, such that control will return through each of those nested functions, or is something else entirely happening here?

The thread's current position is pushed onto the UI message queue. The details are a bit more complex: continuations are scheduled on TaskScheduler.FromCurrentSynchronizationContext unless SynchronizationContext.Current is null, in which case they are scheduled on TaskScheduler.Current. Also, this behavior can be overridden by calling ConfigureAwait(false), which always schedules the continuation on the thread pool. Since SynchronizationContext.Current is a UI SynchronizationContext for WPF/WinForms/Silverlight, this continuation does get pushed onto the UI message queue.

And secondly (while I have your attention), I don't really understand why asynchronous methods need to be labeled with async. Can't any method be executed asynchronously? What if I want to execute a method asynchronously but it doesn't have an async keyword--is there a way to do that simply?

These are slightly different meanings of the word "asynchronous." The async keyword enables the await keyword. In other words, async methods may await. Old-fashioned asynchronous delegates (i.e., BeginInvoke/EndInvoke) is quite different than async. Asynchronous delegates execute on a ThreadPool thread, but async methods execute on the UI thread (assuming that they are called from a UI context and you don't call ConfigureAwait(false)).

If you want to have a (non-async) method run on a ThreadPool thread, you can do it like this:

await Task.Run(() => MyMethod(..));

What I really want to know is to what extent does a continuation continue... does it freeze the entire call stack, to resume it when the task completes, or does it only go back so far? Does a function itself need to be marked as async in order to support continuation, or (as I asked originally) does it continue the entire call stack?

The current position is captured, and "resumed" when the continuation runs. Any function that uses await to support continuations must be marked async.

If you're calling an async method from a non-async method, then you must deal with the Task object directly. This is not normally done. Top-level async methods may return void, so there's no reason not to have async event handlers.

Note that async is purely a compiler transform. That means that async methods are exactly like regular methods after they're compiled. The .NET runtime does not treat them in any special way.


  • I don't think that's true about call stacks. If you throw an exception after an await expression has definitely caused a yield to occur, the exception will not be captured by any catch blocks that were in the original call stack. - Damien_The_Unbeliever
  • No; I just ran a test just to be 100% sure. The exception was captured just fine. Note that the exception is wrapped in a Task, so you do need to await it to see the exception. But then it travels up the call stack just fine. - Stephen Cleary
  • I think you're talking about a situation where the entire (or most) of the callers in the call stack have all been transformed to use async. I'm not sure whether or not that's the image in the head of the OP. I was picturing a stack of non-async code calling an async method, and returning a task. Once that task is returned (and "before" the async method completes), all of the non-async methods in the stack are finished. - Damien_The_Unbeliever
  • I see what you're saying. In that case, the exception would be captured in the Task object, to be observed later. Even if the full stack was captured (which it's not, in this case), the catch blocks wouldn't be run because the exception is captured into the Task. - Stephen Cleary

4

It depends on the behavior of Awaitable.

It has the option to run synchronous, i.e. it runs on the thread and returns control back to the awaiter on the same thread.

If it chooses to run asynchronously, the awaiter will be called back on the thread that the awaitable schedules the callback on. In the meantime the calling thread is released, since there is awaitable started it's asynchronous work and exited and the awaiter has had its continuation attached to callback of the awaitable.

As to your second question, the async keyword is not about whether the method is called asynchronously or not, but whether the body of that method wants to call async code inline itself.

I.e. any method returning Task or Task can be called asynchronously (awaited or with continuewith), but by also marking it with async, that method can now use the await keyword in its body and when it returns it doesn't return a Task, but simply T, since the entire body will be rewritten into a state machine that the Task executes.

Let's say you have method

public async Task DoSomething() {
}

when you call it from the UI thread you get back a task. At this point, you can block on the Task with .Wait() or .Result which runs the the async method on its TaskScheduler or block with .RunSynchronously() which will run it on the UI thread. Of course any await that occurs inside DoSomething is basically another continuation, so it might end up running part of the code on a TaskScheduler thread. But in the end the UI thread is blocked until completion, just like a regular synchronous method.

Or you can schedule a continuation with .ContinueWith() which will create an action that gets called by the TaskScheduler when the Task completes. This will immediately return control to the current code which continues to do whatever it was doing on the UI thread. The continuation doesn't capture the callstack, it's simply an Action, so it merely captures any variables it access from its outer scope.


  • What I specifically want to know is if the entire callstack is set as a continuation, and how this works on the UI thread, since the UI thread can't really exit. - devios1
  • An async method is rewritten by the compiler into a state machine. When the method "suspends" on the await, basically the state machine has captured the current state of all variables in scope. So, no it doesn't do anything to the callstack. - Arne Claassen
  • Regarding the UI thread: When you call an async method (i.e. the only place await can be used), you get back a Task. So if you do that on the UI thread you are either going to block on the Task (blocking the UI thread), or adding a ContinueWith which allows to continue on the UI thread - Arne Claassen

Linked


Related

Latest