我可以递归调用异步函数而不会溢出堆栈吗?

时间:2014-08-22 10:23:42

标签: c# asynchronous

由于async函数的返回站点不是调用者,我认为这有效,但我认为我会验证这是安全的以防万一。如果不是,为什么会溢出堆栈呢?

static async Task CheckAsync(TimeSpan recursiveTimer)
{
    // do some work

    await Task.Delay(recursiveTimer);
    CheckAsync(recursiveTimer);
}

编辑: 我决定尝试一下 - 看起来它不会溢出堆栈(它现在在我的机器上运行 - 它目前正在调用210,000)。我假设的原因是因为CheckAsync函数的返回站点实际上不是CheckAsync,而是异步管道中的某个地方。因此,当CheckAsync调用CheckAsync时,它实际上并不是通过普通函数调用机制添加到调用堆栈,而是将该函数作为对象放在某个异步“待执行”队列中,该队列通过管理异步函数的其他线程运行。

对于任何了解这种机制的人:这听起来是对的吗?

2 个答案:

答案 0 :(得分:5)

它为你工作的原因不是因为调用CheckAsync的方式,而是因为你正在等待Task.Delay的结果。这将永远回归"尚未完成"任务,所以等待它将安排继续。该延续将在有效的空堆栈上触发,因此您进行递归调用并不重要。

现在,我认为你仍然有效地存在内存泄漏,因为IIRC框架将跟踪一个"逻辑堆栈"它将变得越来越大......但是它将被存储在堆上并扩展直到你的内存不足。

如果你想看到堆栈爆炸,你需要做的就是将代码更改为:

static async Task CheckAsync(TimeSpan recursiveTimer)
{
    // Whatever
    await Task.FromResult(5);
    CheckAsync(recursiveTimer);
}

此时,假设代码在"无论什么"没有等待任何事情,您将拥有完全同步的代码,只需使用Task来跟踪完成和异常。

我当然不会推荐这个作为重复工作的模式(部分是由于我提到的内存泄漏),但我希望这能解释为什么你没有得到堆栈溢出。

答案 1 :(得分:1)

溢出的原因是溢出堆栈。

10 static async Task CheckAsync(TimeSpan recursiveTimer)
20 {
30    // do some work
40    await Task.Delay(recursiveTimer);
50    CheckAsync(recursiveTimer);
60 }

代码执行将

10 20 30 40 50 60                         //CheckAsync(recursiveTimer)
            10 20 30 40 50 60             //CheckAsync(CheckAsync(recursiveTimer))
                        10 20 30 40 50 60 //CheckAsync(CheckAsync(CheckAsync(recursiveTimer))

该方法的内容应该是:

while(doCheck){
  await Task.Delay(recursiveTimer);
  CheckAsync(recursiveTimer);
}

假设你想在每次异步调用后等待,并在 doCheck 为真时执行此操作。我假设您在另一个帖子中更改 doCheck 值。

看看:Best Practices in Asynchronous Programming