异步递归。 我的记忆在哪里?

这更多地是出于好奇而不是任何现实世界的问题。

请考虑以下代码:

void Main() { FAsync().Wait(); } async Task FAsync() { await Task.Yield(); await FAsync(); } 

在同步世界中,这最终会导致堆栈溢出。

在异步世界中,这只会消耗大量内存(我认为这与我可能松散地称之为“异步堆栈”的内容有关?)

这些数据到底是什么,以及如何保存?

好问题。

堆栈是延续的具体化。 简单地说,继续是关于该计划下一步将要做什么的信息。 在传统的非异步环境中,这表示为堆栈上的返回地址; 当方法返回时,它查看堆栈并分支到返回地址。 堆栈上还有关于局部变量值在延续点处的位置的信息。

在异步情况下,所有信息都存储在堆上。 任务包含在任务完成时调用的委托。 委托绑定到“闭包”类的实例,该类包含任何局部变量或其他状态的字段。 当然,任务本身就是堆对象。

您可能想知道:如果延续是在任务完成时调用的委托,那么完成任务的代码如何在执行完成时不在调用堆栈上? 任务可以选择通过发布Windows消息来调用continuation委托,并且当消息循环处理消息时,它会执行调用。 因此,调用位于堆栈的“顶部”,消息循环通常位于堆栈的“顶部”。 (用于延续的调用策略的确切细节取决于创建任务的上下文;有关详细信息,请参阅任务并行库的更高级指南。)

有关这一切如何运作的好文章可以在这里找到:

https://msdn.microsoft.com/en-us/magazine/hh456403.aspx

自从Mads撰写该文章以来,一些细节已经改变,但这些想法是合理的。 (i3arnon的答案说明了这种演变的方式;在Mads的文章中,所有内容都在堆上,但在某些情况下,这会产生过多的垃圾。更复杂的codegen允许我们在堆栈上保留一些信息。理解这种区别不是有必要了解如何以逻辑方式表示延续。)

这是一个有趣和有启发性的练习,可以获取您的程序并实际绘制出创建的所有委托和任务,以及它们之间的引用。 试一试!

编译器将您的异步方法转换为状态机结构。 结构首先在堆栈上创建。 当您等待未完成的任务(否则它继续同步运行并将导致堆栈溢出)时,状态机被装箱并移动到堆中。

例如这个方法:

 public async Task M() { } 

变成这个状态机:

 private struct d__0 : IAsyncStateMachine { public int <>1__state; public AsyncTaskMethodBuilder <>t__builder; void IAsyncStateMachine.MoveNext() { try { } catch (Exception exception) { this.<>1__state = -2; this.<>t__builder.SetException(exception); return; } this.<>1__state = -2; this.<>t__builder.SetResult(); } [DebuggerHidden] void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { this.<>t__builder.SetStateMachine(stateMachine); } } 

因此,在“传统”递归中,每次迭代的状态都存储在堆栈中,因此过多的迭代可能会溢出该内存。 在异步方法中,状态存储在堆上,它也可能溢出(尽管它通常要大得多)。