据我了解yield,如果从迭代器块内部使用该关键字,它将控制流返回到调用代码,并且当再次调用该迭代器时,它将从中断的地方开始。
yield
同样,await不仅要等待被调用方,而且还会将控制权返回给调用方,仅在调用方awaits方法时从中断处接管。
await
awaits
换句话说,没有线程,异步和等待的“并发性”是由聪明的控制流引起的错觉,其细节被语法隐藏了。
现在,我是一名前汇编程序员,并且对指令指针,堆栈等非常熟悉,并且了解了正常的控制流程(子例程,递归,循环,分支)的工作方式。但是这些新结构-我不明白。
当await到达,如何运行时知道什么是一段代码下一步应该执行?它如何知道何时可以从上次中断的地方恢复,以及如何记住在哪里?当前调用堆栈发生了什么,是否以某种方式保存了它?如果调用方法在此之前进行其他方法调用await怎么办- 为什么堆栈不被覆盖?在异常和堆栈展开的情况下,运行时到底将如何处理所有这些问题?
何时yield到达,运行时如何跟踪应该拾取的点?迭代器状态如何保存?
我将在下面回答您的特定问题,但是您可能会很容易阅读我关于如何设计产量和等待时间的大量文章。
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)所有locals和temparies的值。
那里还有一些其他装备;例如,在.NET中,分支到try块的中间是非法的,因此您不能简单地将try块内的代码地址粘贴到表中。但是这些是簿记细节。从概念上讲,激活记录只是移到堆上。
当前调用堆栈发生了什么,是否以某种方式保存了它?
当前激活记录中的相关信息永远不会放在栈上。它是从一开始就从堆中分配的。(嗯,形式参数通常在堆栈上或寄存器中传递,然后在方法开始时复制到堆位置。)
呼叫者的激活记录未保存;请记住,等待可能会回到他们身边,所以他们将得到正常处理。
请注意,这是简化的继续等待传递样式与您在诸如Scheme之类的语言中看到的真正的当前循环调用结构之间的紧密区别。在这些语言中,整个延续(包括回到呼叫者的延续)由call- cc捕获。
如果调用方法在等待之前进行其他方法调用,怎么办?为什么堆栈不被覆盖?
这些方法调用返回,因此在等待时它们的激活记录不再在堆栈上。
在异常和堆栈展开的情况下,运行时到底将如何处理所有这些问题?
如果发生未捕获的异常,则捕获该异常并将其存储在任务中,并在获取任务的结果时将其重新抛出。
还记得我之前提到的所有簿记吗?让我告诉你,正确设置异常语义是一个巨大的痛苦。
当达到产量时,运行时如何跟踪应该拾取的点?迭代器状态如何保存?
同样的方式。当地人的状态被移到堆上,并且代表该指令的数字MoveNext在下一次被调用时应在该指令处重新存储。
MoveNext
同样,在迭代器块中还有很多工具可以确保正确处理异常。