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

本文关键字:溢出 堆栈 递归 调用 异步 函数 我可以 | 更新日期: 2023-09-27 18:19:09

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

static async Task CheckAsync(TimeSpan recursiveTimer)
{
    // do some work
    await Task.Delay(recursiveTimer);
    CheckAsync(recursiveTimer);
}

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

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

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

它为您工作的原因不是因为CheckAsync被调用的方式,而是因为您正在等待Task.Delay的结果。这将始终返回一个"尚未完成"的任务,因此等待它将安排一个延续。该延续将在一个有效的空堆栈上触发,因此您随后进行递归调用无关紧要。

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

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

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

此时,假设"whatever"中的代码不等待任何东西,您将拥有完全同步的代码,仅使用Task来跟踪完成和异常。

我当然不建议将此作为重复执行工作的模式(部分原因是我提到的内存泄漏),但我希望这可以解释为什么您不会得到堆栈溢出。

溢出的原因是溢出堆栈。

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值。

请看:异步编程的最佳实践