【问题标题】:StackOverflowExceptions in nested async methods on unwinding of the stack堆栈展开时嵌套异步方法中的 StackOverflowExceptions
【发布时间】:2017-06-26 13:05:13
【问题描述】:

我们有很多嵌套的异步方法,并且看到了我们并不真正理解的行为。以这个简单的 C# 控制台应用程序为例

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncStackSample
{
  class Program
  {
    static void Main(string[] args)
    {
      try
      {
        var x = Test(index: 0, max: int.Parse(args[0]), throwException: bool.Parse(args[1])).GetAwaiter().GetResult();
        Console.WriteLine(x);
      }
      catch(Exception ex)
      {
        Console.WriteLine(ex);
      }
      Console.ReadKey();
    }

    static async Task<string> Test(int index, int max, bool throwException)
    {
      await Task.Yield();

      if(index < max)
      {
        var nextIndex = index + 1;
        try
        {
          Console.WriteLine($"b {nextIndex} of {max} (on threadId: {Thread.CurrentThread.ManagedThreadId})");

          return await Test(nextIndex, max, throwException).ConfigureAwait(false);
        }
        finally
        {
          Console.WriteLine($"e {nextIndex} of {max} (on threadId: {Thread.CurrentThread.ManagedThreadId})");
        }
      }

      if(throwException)
      {
        throw new Exception("");
      }

      return "hello";
    }
  }
}

当我们使用以下参数运行此示例时:

AsyncStackSample.exe 2000 false

我们得到一个 StackOverflowException,这是我们在控制台中看到的最后一条消息:

e 331 of 2000 (on threadId: 4)

当我们把参数改成

AsyncStackSample.exe 2000 true

我们以这条消息结束

e 831 of 2000 (on threadId: 4)

所以 StackOverflowException 发生在堆栈展开时(不确定我们是否应该调用它,但 StackOverflowException 在我们的示例中递归调用之后发生,在同步代码中,StackOverflowException 将始终发生在嵌套方法调用上)。在我们抛出异常的情况下,StackOverflowException 会更早发生。

我们知道我们可以通过在 finally 块中调用 Task.Yield() 来解决这个问题,但是我们有几个问题:

  1. 为什么堆栈会在展开路径上增长(与 在等待时不会导致线程切换的方法)?
  2. 为什么 StackOverflowException 在 Exception 情况下比我们不抛出异常时更早出现?

【问题讨论】:

  • 感谢@Bit,虽然它涉及相同的主题,但它没有回答我们的具体问题。
  • 也有关系,但我从来没有考虑过回程:stackoverflow.com/questions/35464468/…
  • 我运行它并得到了和你一样的结果。当我删除对await Task.Yield() 的调用时,它会一直展开堆栈直到开始。然后我尝试通过将第一个参数设为 3000 来增加迭代次数,这在e 1568 of 3000 (on threadId: 5) 上引发了相同的异常

标签: c# .net async-await stack-overflow


【解决方案1】:

为什么堆栈会在展开路径上增长(与在等待时不会导致线程切换的方法相比)?

核心原因是因为await schedules its continuations with the TaskContinuationOptions.ExecuteSynchronously flag

因此,当执行“最内层”Yield 时,您最终会得到 3000 个未完成的任务,每个“内层”任务都持有一个完成回调来完成下一个最内层任务。这一切都在堆中。

当最里面的Yield 恢复时(在线程池线程上),延续(同步)执行Test 方法的剩余部分,该方法完成它的任务,它(同步)执行Test 的剩余部分方法,它完成 its 任务等几千次。因此,随着每个任务的完成,该线程池线程上的调用堆栈实际上增长

就个人而言,我发现这种行为令人惊讶,并将其报告为错误。但是,该错误已被 Microsoft 关闭为“设计使然”。有趣的是,JavaScript 中的 Promises 规范(以及扩展,await 的行为)总是 具有异步运行的 Promise 完成,而 从不 同步运行。这让一些 JS 开发人员感到困惑,但这是我所期望的行为。

通常情况下,它运行良好,ExecuteSynchronously 作为一个小的性能改进。但正如您所指出的,在某些情况下,例如“异步递归”可能会导致 StackOverflowException

一些heuristics in the BCL to run continuations asynchronously if the stack is too full,但它们只是启发式方法,并不总是有效。

为什么 StackOverflowException 在 Exception 情况下比我们不抛出异常时更早出现?

这是一个很好的问题。我不知道。 :)

【讨论】:

  • 感谢斯蒂芬的精彩回答。知道如何回答第二个问题吗?
  • 您可以将其作为一个单独的问题提出。这样它就非常具体了——只有一个问题。
  • github.com/dotnet/roslyn/issues/26567 似乎是第二个问题的答案。
猜你喜欢
  • 1970-01-01
  • 2011-08-13
  • 1970-01-01
  • 1970-01-01
  • 2013-12-25
  • 2014-04-17
  • 2012-07-31
  • 2018-04-01
  • 1970-01-01
相关资源
最近更新 更多