我将在下面回答您的特定问题,但是您可能会很容易阅读我有关如何设计产量和等待时间的大量文章。
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/
这些文章中有些已经过时了。生成的代码在很多方面都不同。但是,这些肯定会让您了解其工作原理。
另外,如果您不了解lambda如何作为闭包类生成,请首先了解。如果没有lambda,那么您将不会做出异步的事情。
当到达等待状态时,运行时如何知道接下来应该执行什么代码?
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
基本上就是这样。等待只是幻想的回报。
它如何知道何时可以从上次中断的地方恢复?如何记住在哪里?
好吧,您如何在不等待的情况下做到这一点?当方法foo调用方法bar时,无论如何执行bar,我们都以某种方式记得如何回到foo的中间,而激活foo的所有本机都保持不变。
您知道在汇编器中是如何完成的。foo的激活记录被压入堆栈;它包含本地人的值。在调用时,将foo中的返回地址压入堆栈。完成bar之后,堆栈指针和指令指针将重置为所需的位置,而foo将从其中断处继续运行。
等待的继续是完全相同的,除了将记录放到堆上是出于明显的原因,即激活序列没有形成堆栈。
等待任务继续执行的委托包含(1)一个数字,该数字是查找表的输入,该查询表提供了接下来需要执行的指令指针,以及(2)所有本地和临时值。
那里还有一些其他装备;例如,在.NET中,分支到try块的中间是非法的,因此您不能简单地将try块内的代码地址粘贴到表中。但是这些是簿记细节。从概念上讲,激活记录只是移到堆上。
当前调用堆栈发生了什么,是否以某种方式保存了它?
当前激活记录中的相关信息永远不会放在栈上。它是从一开始就从堆中分配的。(嗯,形式参数通常在堆栈上或寄存器中传递,然后在方法开始时复制到堆位置。)
呼叫者的激活记录未保存;请记住,等待可能会回到他们身边,所以他们将得到正常处理。
请注意,这是简化的继续等待传递样式与您在诸如Scheme之类的语言中看到的真正的当前通话继续结构之间的紧密区别。在这些语言中,整个延续(包括回到呼叫者的延续)由call-cc捕获。
如果调用方法在等待之前进行其他方法调用怎么办-为什么堆栈不被覆盖?
这些方法调用返回,因此在等待时它们的激活记录不再在堆栈上。
在异常和堆栈展开的情况下,运行时到底如何处理所有这些问题?
如果发生未捕获的异常,则捕获该异常,将其存储在任务中,并在获取任务的结果时将其重新抛出。
还记得我之前提到的所有簿记吗?让我告诉你,正确设置异常语义是一个巨大的痛苦。
当达到产量时,运行时如何跟踪应该拾取的点?迭代器状态如何保存?
同样的方法。当地人的状态被移到堆上,并且代表该指令的数字MoveNext
在下一次被调用时应在该指令处重新存储。
同样,在迭代器块中还有很多工具可以确保正确处理异常。