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.

Leave a Comment