【问题标题】:Async Await Recursion in .NET 4.8 causes StackoverflowException (not in .Net Core 3.1!).NET 4.8 中的异步等待递归导致 StackoverflowException(不在 .Net Core 3.1 中!)
【发布时间】:2020-04-08 14:32:53
【问题描述】:

为什么下面的代码在.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);
}

这是 .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);
  }
}

只有在选择 Any Cpu 并选择 32 位禁用 时才会如此快速,但在多个 .net 版本的多台机器(Windows 1903 和 1909)上可重现(.Net 4.7.2 和 .Net 4.8)

【问题讨论】:

  • count &lt; 0 时抛出。但是在TestAsync6 中,您使用count-1 调用TestAsync。不管你是从 17 还是 2 开始,迟早你都会用-1 调用它,然后得到异常。我会说这是意料之中的。
  • 嗯,是的,当count &lt;= 0 时它会抛出,当用2 调用它时这很好,而不是它会抛出,异常将被捕获并且一切都很好,但是当用@987654331 调用时@ 异常会被抛出,但又会导致堆栈溢出异常,这是我没想到的......
  • 似乎存在一个相关问题,即 OP 在 .NET Framework 4.7.2 上遇到了与访问者模式相同的问题,但假设异常为什么是 StackOverflowException。看看:stackoverflow.com/questions/61102165/…
  • 我只是仔细检查了一遍。在 20000 的 Debug 和 Release 中运行在 4.7.2 下。作为一个魅力。
  • 我能够在 .NET Framework 4.8 中使用 await TestAsync(235); 重现它。但在这一点上,这是意料之中的。

标签: c# recursion async-await


【解决方案1】:

我怀疑您在 completions 上看到了 Stack Overflow - 即,在 Stack Overflow 消息之前,每个数字都会一直打印到 1

我的猜测是这种行为是因为await uses synchronous continuations。有supposed to be code that prevents synchronous continuations from overflowing the stack,但它是启发式的,并不总是有效。

我怀疑这种行为不会在 .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 次调用深度)。

请记住,这只发生在 tail recursive 异步函数的长序列中 - 即,在它们的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
  }
}

【讨论】:

  • 是的,你是对的,我更新了我的问题以提供一个更简单的示例,表明 97 个直接递归级别是关键点。然而,在为禁用 Prefer 32 位的任何 CPU 编译时,它只有那么低,所以那里肯定有更多的事情发生。我还用 yield 测试了版本,它确实解决了问题,但是在我们的真实场景(访问者)中,我们有一些访问方法没有工作......所以我仍然认为这是一个错误
  • 我同意这是一个错误。我只是不认为它会被修复。
  • 我们将在未来几周内向 Microsoft 发起支持电话,我会在收到回复后立即更新此问题
  • 由于#23152,它适用于.NET Core。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-02
  • 2013-11-13
  • 1970-01-01
  • 2023-03-24
  • 2014-02-15
  • 1970-01-01
相关资源
最近更新 更多