.NET 4.8 中的异步等待递归导致 StackoverflowException(不在 .Net Core 3.1 中!)

Ins*_*yro 5 c# recursion async-await

为什么下面的代码在 .Net4.8 中导致 StackOverflowException 只有 17 深度递归?但是这在 NetCore 3.1 中不会发生(我可以将计数设置为 10_000 并且它仍然有效)

class Program
{
  static async Task Main(string[] args)
  {
    try
    {
      await TestAsync(17);
    }
    catch(Exception e)
    {
      Console.WriteLine("Exception caught: " + e);
    }
  }

  static async Task TestAsync(int count)
  {
    await Task.Run(() =>
    {
      if (count <= 0)
        throw new Exception("ex");
    });

    Console.WriteLine(count);
    await TestAsync2(count);
  }

  static async Task TestAsync2(int count) => await TestAsync3(count);
  static async Task TestAsync3(int count) => await TestAsync4(count);
  static async Task TestAsync4(int count) => await TestAsync5(count);
  static async Task TestAsync5(int count) => await TestAsync6(count);
  static async Task TestAsync6(int count) => await TestAsync(count - 1);
}
Run Code Online (Sandbox Code Playgroud)

这是 .Net 4.8 中的已知错误吗?我会在这样的函数中排除超过 17 个级别的递归......这是否实际上意味着不推荐使用 async/await 编写递归?

更新:简化版

class Program
{
  // needs to be compiled as AnyCpu Prefer 64-bit
  static async Task Main(string[] args)
  {
    try
    {
      await TestAsync(97); // 96 still works
    }
    catch(Exception e)
    {
      Console.WriteLine("Exception caught: " + e);
    }
  }

  static async Task TestAsync(int count)
  {
    await Task.Run(() =>
    {
      if (count <= 0)
        throw new Exception("ex");
    });

    Console.WriteLine(count);
    await TestAsync(count-1);
  }
}
Run Code Online (Sandbox Code Playgroud)

在选择Any Cpu with Prefer 32-bit disabled 时,它只会发生得如此之快,但可在多个 .net 版本(.Net 4.7.2 和 .Net 4.8)上的多台机器(Windows 1903 和 1909)上重现

Ste*_*ary 5

我怀疑您在完成时看到了堆栈溢出- 即,每个数字都一直打印到1堆栈溢出消息之前。

我的猜测是,这种行为是因为await使用了同步延续。应该代码可以防止同步延续溢出堆栈,但它是启发式的,并不总是有效。

我怀疑这种行为不会发生在 .NET Core 上,因为大量优化工作已经投入到 .NET Core 的async支持中,这可能意味着该平台上的延续占用更少的堆栈空间,从而使启发式检查发挥作用。启发式本身也可能已在 .NET Core 中修复。无论哪种方式,我都不会屏息期待 .NET Framework 获得这些更新。

我会在这样的函数中排除超过 17 级的递归......

不是真的 17。你有 102 级递归 ( 17 * 6)。要测量实际占用的堆栈空间,它将是17 * 6 * (number of stacks to resume continuations)。在我的机器上,17 个有效;它在超过 200 个地方失败(1200 个深度调用)。

请记住,这只发生在尾递归异步函数的长序列上 - 即,它们中没有一个await. 如果您将任何函数更改为在递归之后await进行一些其他异步工作,这将避免堆栈溢出:

static async Task TestAsync(int count)
{
  await Task.Run(() =>
  {
    if (count <= 0)
      throw new Exception("ex");
  });

  Console.WriteLine(count);
  try
  {
    await TestAsync2(count);
  }
  finally
  {
    await Task.Yield(); // some other async work
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 我们将在未来几周内向 Microsoft 发起支持电话,我将在收到回复后立即更新此问题 (2认同)