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

时间:2016-02-17 18:11:52

标签: c# memory recursion async-await callstack

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

请考虑以下代码:

void Main()
{
    FAsync().Wait();
}

async Task FAsync()
{
    await Task.Yield();
    await FAsync();
}

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

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

这些数据究竟是什么,以及如何保存?

2 个答案:

答案 0 :(得分:12)

好问题。

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

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

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

这里有一篇关于这一切如何运作的好的介绍性文章:

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

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

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

答案 1 :(得分:3)

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

例如这个方法:

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);
    }
}

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