我将在下面回答你的具体问题,但你最好仍然阅读我们如何设计yield和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 会被生成为:
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
基本上就是这样。Await只是一个花哨的返回。
它如何知道在哪里可以恢复之前的操作,以及如何记住位置呢?
那么,如果没有await,你怎么做到这一点呢?当方法foo调用方法bar时,我们以某种方式记住了如何回到foo的中间,并保持foo激活时的所有本地内容不变,无论bar做了什么。
你知道汇编语言是如何做到这一点的。为foo创建一个激活记录并将其推入堆栈;它包含本地内容的值。在调用点,foo中的返回地址被推入堆栈。当bar完成后,堆栈指针和指令指针被重置到需要的位置,并且foo从离开的地方继续执行。
等待的继续与此完全相同,只是该记录被放置在堆上,原因很明显,即“激活序列的顺序不形成堆栈”。
await作为任务给出的委托包含(1)一个数字,它是查找表的输入,该表给出您需要执行的下一个指令指针,以及(2)所有本地变量和临时变量的值。
这里有一些额外的装备;例如,在.NET中,分支到try块的中间是非法的,因此您不能简单地将代码地址放入表格中的try块中。但这些都是簿记细节。从概念上讲,活动记录只是移动到堆上。
当前调用栈会发生什么,它会以某种方式保存吗?
当前激活记录中的相关信息从一开始就不会放在堆栈上;它们是从堆中分配的。(好吧,形式参数通常在堆栈或寄存器中传递,然后在方法开始时复制到堆位置。)
调用者的激活记录不会被存储;请记住,await可能会返回给它们,因此它们将按正常方式处理。
请注意,这是await简化的延续传递样式和真正的带有当前延续结构之间的一个相关区别,您可以在像Scheme这样的语言中看到整个包括返回调用者的延续的call-cc。
如果调用方法在等待之前进行其他方法调用,为什么堆栈不会被覆盖?
那些方法调用返回后,它们的激活记录在等待点不再存在于堆栈上。
如果发生未捕获的异常,异常会被捕获并存储在任务中,在获取任务结果时重新抛出。
记住我之前提到的所有簿记吗?正确处理异常语义是非常困难的。
当到达 yield 时,运行时如何跟踪应该恢复的点?迭代器状态如何保留?
同样的方式。局部变量的状态被移动到堆上,并存储一个数字表示 MoveNext 下一次被调用时应恢复的指令与局部变量一起。
而且,有一堆迭代器块中的工具来确保正确处理异常。