异步递归。我的记忆实际上去了哪里?
Async recursion. Where is my memory actually going?
问这个问题更多是出于好奇,而不是针对任何现实世界的问题。
考虑以下代码:
void Main()
{
FAsync().Wait();
}
async Task FAsync()
{
await Task.Yield();
await FAsync();
}
在同步世界中,这最终会导致计算器溢出。
在异步世界中,这只会消耗大量内存(我认为这与我可能松散地称为 "asynchronous stack" 的东西有关?)
这些数据到底是什么,它是如何保存的?
好问题。
堆栈是continuation的具体化。简单来说,继续就是关于程序接下来要做什么的信息。在传统的非异步环境中,这表示为堆栈上的 return 地址;当方法 returns 它查看堆栈并分支到 return 地址。堆栈上还有关于局部变量的值在延续开始时的信息。
在异步情况下,所有信息都存储在堆上。任务包含一个在任务完成时调用的委托。委托绑定到 "closure" class 的实例,其中包含任何局部变量或其他状态的字段。当然任务本身就是堆对象。
您可能想知道:如果延续是在任务完成时调用的委托,那么完成任务的代码怎么不在调用堆栈上 在执行完成的地方?任务可以选择通过发布windows消息来调用延续委托,当消息循环处理消息时,它会进行调用。因此,调用位于堆栈的 "top" 处,消息循环通常位于此处。 (用于延续的调用策略的精确细节取决于创建任务的上下文;有关详细信息,请参阅任务并行库的更高级指南。)
可以在此处找到关于这一切如何工作的很好的介绍性文章:
https://msdn.microsoft.com/en-us/magazine/hh456403.aspx
自 Mads 撰写那篇文章以来,一些细节发生了变化,但想法是正确的。 (i3arnon 的回答说明了这是如何演变的;在 Mads 的文章中,一切都在堆上,但在某些情况下这会产生过多的垃圾。更复杂的代码生成器允许我们在堆栈上保留一些信息。理解这种区别并不重要有必要了解延续是如何在逻辑上表示的。)
执行您的程序并实际绘制出所有创建的委托和任务,以及它们之间的引用是什么,这是一项有趣且富有启发性的练习。试一试!
编译器将您的异步方法转换为状态机结构。该结构首先在堆栈上创建。当您等待未完成的任务时(否则它会继续 运行 同步并导致堆栈溢出)状态机被装箱并移动到堆中。
例如这个方法:
public async Task M()
{
}
变成这个状态机:
private struct <M>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);
}
}
因此,在 "traditional" 递归中,每次迭代的状态都存储在堆栈中,因此过多的迭代会溢出内存。在异步方法中,状态存储在堆上,它也可能溢出(尽管它通常要大得多)。
问这个问题更多是出于好奇,而不是针对任何现实世界的问题。
考虑以下代码:
void Main()
{
FAsync().Wait();
}
async Task FAsync()
{
await Task.Yield();
await FAsync();
}
在同步世界中,这最终会导致计算器溢出。
在异步世界中,这只会消耗大量内存(我认为这与我可能松散地称为 "asynchronous stack" 的东西有关?)
这些数据到底是什么,它是如何保存的?
好问题。
堆栈是continuation的具体化。简单来说,继续就是关于程序接下来要做什么的信息。在传统的非异步环境中,这表示为堆栈上的 return 地址;当方法 returns 它查看堆栈并分支到 return 地址。堆栈上还有关于局部变量的值在延续开始时的信息。
在异步情况下,所有信息都存储在堆上。任务包含一个在任务完成时调用的委托。委托绑定到 "closure" class 的实例,其中包含任何局部变量或其他状态的字段。当然任务本身就是堆对象。
您可能想知道:如果延续是在任务完成时调用的委托,那么完成任务的代码怎么不在调用堆栈上 在执行完成的地方?任务可以选择通过发布windows消息来调用延续委托,当消息循环处理消息时,它会进行调用。因此,调用位于堆栈的 "top" 处,消息循环通常位于此处。 (用于延续的调用策略的精确细节取决于创建任务的上下文;有关详细信息,请参阅任务并行库的更高级指南。)
可以在此处找到关于这一切如何工作的很好的介绍性文章:
https://msdn.microsoft.com/en-us/magazine/hh456403.aspx
自 Mads 撰写那篇文章以来,一些细节发生了变化,但想法是正确的。 (i3arnon 的回答说明了这是如何演变的;在 Mads 的文章中,一切都在堆上,但在某些情况下这会产生过多的垃圾。更复杂的代码生成器允许我们在堆栈上保留一些信息。理解这种区别并不重要有必要了解延续是如何在逻辑上表示的。)
执行您的程序并实际绘制出所有创建的委托和任务,以及它们之间的引用是什么,这是一项有趣且富有启发性的练习。试一试!
编译器将您的异步方法转换为状态机结构。该结构首先在堆栈上创建。当您等待未完成的任务时(否则它会继续 运行 同步并导致堆栈溢出)状态机被装箱并移动到堆中。
例如这个方法:
public async Task M()
{
}
变成这个状态机:
private struct <M>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);
}
}
因此,在 "traditional" 递归中,每次迭代的状态都存储在堆栈中,因此过多的迭代会溢出内存。在异步方法中,状态存储在堆上,它也可能溢出(尽管它通常要大得多)。