How Do Yield and Await Implement Flow of Control in .Net

How do yield and await implement flow of control in .NET?

I'll answer your specific questions below, but you would likely do well to simply read my extensive articles on how we designed yield and await.

https://blogs.msdn.microsoft.com/ericlippert/tag/continuation-passing-style/

https://blogs.msdn.microsoft.com/ericlippert/tag/iterators/

https://blogs.msdn.microsoft.com/ericlippert/tag/async/

Some of these articles are out of date now; the code generated is different in a lot of ways. But these will certainly give you the idea of how it works.

Also, if you do not understand how lambdas are generated as closure classes, understand that first. You won't make heads or tails of async if you don't have lambdas down.

When an await is reached, how does the runtime know what piece of code should execute next?

await is generated as:

if (the task is not completed)
assign a delegate which executes the remainder of the method as the continuation of the task
return to the caller
else
execute the remainder of the method now

That's basically it. Await is just a fancy return.

How does it know when it can resume where it left off, and how does it remember where?

Well, how do you do that without await? When method foo calls method bar, somehow we remember how to get back to the middle of foo, with all the locals of the activation of foo intact, no matter what bar does.

You know how that's done in assembler. An activation record for foo is pushed onto the stack; it contains the values of the locals. At the point of the call the return address in foo is pushed onto the stack. When bar is done, the stack pointer and instruction pointer are reset to where they need to be and foo keeps going from where it left off.

The continuation of an await is exactly the same, except that the record is put onto the heap for the obvious reason that the sequence of activations does not form a stack.

The delegate which await gives as the continuation to the task contains (1) a number which is the input to a lookup table that gives the instruction pointer that you need to execute next, and (2) all the values of locals and temporaries.

There is some additional gear in there; for instance, in .NET it is illegal to branch into the middle of a try block, so you can't simply stick the address of code inside a try block into the table. But these are bookkeeping details. Conceptually, the activation record is simply moved onto the heap.

What happens to the current call stack, does it get saved somehow?

The relevant information in the current activation record is never put on the stack in the first place; it is allocated off the heap from the get-go. (Well, formal parameters are passed on the stack or in registers normally and then copied into a heap location when the method begins.)

The activation records of the callers are not stored; the await is probably going to return to them, remember, so they'll be dealt with normally.

Note that this is a germane difference between the simplified continuation passing style of await, and true call-with-current-continuation structures that you see in languages like Scheme. In those languages the entire continuation including the continuation back into the callers is captured by call-cc.

What if the calling method makes other method calls before it awaits-- why doesn't the stack get overwritten?

Those method calls return, and so their activation records are no longer on the stack at the point of the await.

And how on earth would the runtime work its way through all this in the case of an exception and a stack unwind?

In the event of an uncaught exception, the exception is caught, stored inside the task, and re-thrown when the task's result is fetched.

Remember all that bookkeeping I mentioned before? Getting exception semantics right was a huge pain, let me tell you.

When yield is reached, how does the runtime keep track of the point where things should be picked up? How is iterator state preserved?

Same way. The state of locals is moved onto the heap, and a number representing the instruction at which MoveNext should resume the next time it is called is stored along with the locals.

And again, there's a bunch of gear in an iterator block to make sure that exceptions are handled correctly.

How does control flow when there is synchronous code in awaited function?

It is true that "when await is encountered, control immediately goes back the caller", under the condition that the awaitable will not be completed at the await point. Otherwise (if the awaitable is already completed) the control will stay inside the method, by executing immediately the code that follows the await.

That's all that influences the behavior of the await operator: the result of the IsCompleted property of the awaiter. It doesn't matter if the method that produced the awaitable included synchronous code or not. The await doesn't know, and doesn't care. A synchronous code block inside the someFn will indeed make a difference regarding the responsiveness of your application, but that has nothing to do with the await. To understand why, it may help to rewrite your code in a more verbose but equivalent form. This:

await someFn();

...is equivalent to this:

Task t = someFn();
await t;

The UI of your application will freeze because the someFn() blocks, not because the await t blocks. Unless of course you have created some wicked task-like awaitable type, that blocks when the IsCompleted property of its awaiter is invoked.

Why is an await Task.Yield() required for Thread.CurrentPrincipal to flow correctly?

How interesting! It appears that Thread.CurrentPrincipal is based on the logical call context, not the per-thread call context. IMO this is quite unintuitive and I'd be curious to hear why it was implemented this way.


In .NET 4.5., async methods interact with the logical call context so that it will more properly flow with async methods. I have a blog post on the topic; AFAIK that's the only place where it's documented. In .NET 4.5, at the beginning of every async method, it activates a "copy-on-write" behavior for its logical call context. When (if) the logical call context is modified, it will create a local copy of itself first.

You can see the "localness" of the logical call context (i.e., whether it has been copied) by observing System.Threading.Thread.CurrentThread.ExecutionContextBelongsToCurrentScope in a watch window.

If you don't Yield, then when you set Thread.CurrentPrincipal, you're creating a copy of the logical call context, which is treated as "local" to that async method. When the async method returns, that local context is discarded and the original context takes its place (you can see ExecutionContextBelongsToCurrentScope returning to false).

On the other hand, if you do Yield, then the SynchronizationContext behavior takes over. What actually happens is that the HttpContext is captured and used to resume both methods. In this case, you're not seeing Thread.CurrentPrincipal preserved from AuthenticateAsync to GetAsync; what is actually happening is HttpContext is preserved, and then HttpContext.User is overwriting Thread.CurrentPrincipal before the methods resume.

If you move the Yield into GetAsync, you see similar behavior: Thread.CurrentPrincipal is treated as a local modification scoped to AuthenticateAsync; it reverts its value when that method returns. However, HttpContext.User is still set correctly, and that value will be captured by Yield and when the method resumes, it will overwrite Thread.CurrentPrincipal.

Async programming control flow

First something important: The runtime will only return to the caller on await if the task to await hasn't completed yet.

Question 1: I'll quote the MSDN here:

The async and await keywords don't cause additional threads to be created. Async methods don't require multithreading because an async method doesn't run on its own thread.

Source

Question 2: Maybe in the implementation of GetStringAsync but we don't know that and we also don't have to know it. It's enough for us to know that GetStringAsync somehow gets its result without blocking our thread.

Question 3: If the loop is placed before the await keyword, yes.

Question 4: Quote from the same paragraph as before:

You can use Task.Run to move CPU-bound work to a background thread

You don't need the async and await keywords for this. Example:

private Task<int> DoSomethingAsync()
{
return Task.Run(() =>
{
// Do CPU heavy calculation here. It will be executed in the thread pool.
return 1 + 1;
});
}

Question 5: I'll quote again

An async method typically contains one or more occurrences of an await operator, but the absence of await expressions doesn’t cause a compiler error. If an async method doesn’t use an await operator to mark a suspension point, the method executes as a synchronous method does, despite the async modifier. The compiler issues a warning for such methods.

Source (same page as before, just a different section)

C# async/await - How does the program keep track of where await was called?

When a method contains an await, the compiler creates what is called an Async State Machine. The task being awaited is stored off and everything after the await is marked as the task's continuation.

Through the state machine, an AsyncTaskMethodBuilder<>, and a TaskAwaiter<>, the infrastructure keeps track of the execution and synchronization contexts, captures local variables and ultimately tracks the state of the original Task while it waits for completion. It does this by looping (calling an internal MoveNext method) until the task has completed, been cancelled or faulted (due to an exception). Assuming the former, the continuation is then invoked on the appropriate context. When an exception is thrown, the AggregateException you would normally get with a Task is "unwrapped" (throwing the first exception) to mimic the experience of a normal, synchronous method.

This is an overall simplification. There are various optimizations and a bunch of machinery that makes this all work. For a good blog on the topic, I suggest this Microsoft series on async methods.

How Async/await works in .net 4.5

Async and await works with the Task Library. If you write a method and want to make it async you just have to mark it as async and call await on any task inside of your method. Just the await keyword make your method async and just this code runs async. For example:

//This Method isn't async because there is no await
private async Task DoSomething()
{
//Some work
}

//This method is async because it awaits sth.
private async Task DoSomething()
{
await SomeOtherStuff();
}

Note that async methods return Task or Task which encapsulate your return type. This Task allows other methods to await your method. Up to this way you build a chain which ends in your GUI. So you GUI isn't blocking and responsive.

I found this diagram after 1 sec of google which describe this behaviour pretty well:

Sample Image

This has not much to do with BeginInvoke and EndInvoke, because Invoke calls are just for using Gui objects in different threads. If possible you should avoid BeginInvoke and EndInvoke and use the GUI only on your Mainthread.

Asp.Net core 2.1 await doesn't yield control to caller on 2nd run

Let's set a couple of things straight:

  1. Threads are not magical resources that are just laying there, w8ing for work
  2. The higher number of threads you have, more resources you´ll need to manage them
  3. Asynchrony, although very helpful is not an easy thing

Ok, first of all, your app starts with a predefined number of w8ing threads (usually number of cores*2, if i recall correctly, can find the damn msdn article). If you request threads and you´re using under this limit, you get them instantaneously, otherswise, you'll have to w8 500ms (cant find the article)

Second, async is not parallel!

Third, in your code, whenever you use await, it awaits! so, i suggest you refactor await on OpenAsync, chaining with ContinueWith and only use the await once in your code, because, as it is right now, your using's are preventing async from working properly... you will have to manage the connections and disposals on your own if you want the code to run asynchronously

Edit1:

There are 2 types of threads: IO Threads and Worker threads. In your code, what you need is IO threads to w8 for the database... if you need more context on this, you can start here



Related Topics



Leave a reply



Submit