它是混合类型的东西吗?(例如,.NET程序是否使用堆栈,直到遇到异步调用,然后切换到其他结构,直到完成,此时堆栈才退回到可以确定是否有下一个项目的状态,依此类推? )
基本上是。
假设我们有
async void MyButton_OnClick() { await Foo(); Bar(); }
async Task Foo() { await Task.Delay(123); Blah(); }
这是对连续化方式进行极简化的解释。实际的代码要复杂得多,但这可以使想法完整。
您单击按钮。一条消息排队。消息循环处理消息并调用单击处理程序,从而将消息队列的返回地址放在堆栈上。也就是说,在处理程序完成之后发生的事情是消息循环必须继续运行。因此,处理程序的延续就是循环。
单击处理程序调用Foo(),将其自身的返回地址放在堆栈中。也就是说,Foo的延续是单击处理程序的其余部分。
Foo调用Task.Delay,将其自身的返回地址放在堆栈中。
Task.Delay会执行立即返回Task所需的所有魔术。堆栈弹出,我们回到Foo中。
Foo检查返回的任务以查看其是否完成。它不是。在连续的中的await是调用布拉赫(),所以富创建这就要求布拉赫()的委托,和迹象表明,委托了作为任务的延续。(我只是做了一个小小的错误陈述;您是否抓住了它?如果没有,我们将在稍后揭露。)
然后,Foo创建自己的Task对象,将其标记为未完成,然后将其返回堆栈到Click处理程序中。
点击处理程序检查Foo的任务,发现它不完整。处理程序中await的继续是调用Bar(),因此单击处理程序将创建一个委托,该委托调用Bar()并将其设置为Foo()返回的任务的继续。然后,它将堆栈返回到消息循环。
消息循环继续处理消息。最终,由延迟任务创建的计时器魔术完成了它的工作,并向队列中发布了一条消息,表明现在可以执行延迟任务的继续。因此,消息循环调用任务继续,将其照常放入堆栈中。该代表调用Blah()。Blah()执行其操作并返回堆栈。
现在会发生什么?这有点棘手。延迟任务的继续不仅会调用Blah()。 它也必须触发对Bar()的调用,但是该任务对Bar不了解!
Foo 实际上创建了一个委托,该委托(1)调用Blah(),而(2)调用Foo创建的任务的继续并将其交还给事件处理程序。这就是我们所谓的调用Bar()的委托的方式。
现在,我们已经按照正确的顺序完成了所有需要做的事情。但是,我们从未停止在消息循环中处理消息很长时间,因此应用程序仍保持响应状态。
这些场景对于堆栈而言太高级了,这是很合理的,但是什么能代替堆栈呢?
任务对象图,其中包含通过委托的闭合类相互引用。这些闭包类是状态机,它们跟踪最近执行的等待位置和本地值。另外,在给出的示例中,由操作系统和执行这些操作的消息循环实现的操作的全局状态队列。
练习:您如何假设这一切在没有消息循环的世界中有效?例如,控制台应用程序。在控制台应用程序中等待是完全不同的;您能从到目前为止的知识中推断出它是如何工作的吗?
几年前,当我了解到这一点时,堆栈就在那里了,因为它闪电般快速且轻巧,因为它支持对即将完成的任务进行高效管理(双关语?),所以它在应用程序中从堆分配了一块内存。有什么变化?
当方法激活的有效期形成堆栈时,堆栈是有用的数据结构,但在我的示例中,单击处理程序Foo,Bar和Blah的激活不形成堆栈。因此,代表工作流程的数据结构不能是堆栈。相反,它是代表工作流的堆已分配任务和委托的图形。等待是工作流中的点,在这些点上,直到较早开始的工作完成,才能在工作流中进一步取得进展。在我们等待,我们可以执行其他的,它的工作不依赖于在完成这些特定的启动任务。
堆栈只是一个框架数组,其中框架包含(1)指向函数中间(调用发生的位置)的指针和(2)局部变量和临时变量的值。任务的延续是同一回事:委托是指向函数的指针,并且它具有一个状态,该状态引用函数中间的特定点(发生等待的位置),并且闭包具有用于每个局部变量或临时变量的字段。框架不再构成一个很好的整洁数组,但是所有信息都是相同的。