StackOverflowException

StackOverflowException

我们有很多嵌套的异步方法,并且看到了我们并不真正了解的行为。以这个简单的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()解决此问题,但是我们有几个问题:
  • 为什么堆栈在展开路径上增长(与
    不会在等待状态下导致线程切换的方法)?
  • 为什么在Exception情况下,StackOverflowException发生的时间比不抛出异常时要早?
  • 最佳答案



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

    因此,当执行“最里面”的Yield时,最终得到的是3000个未完成的任务,每个“内”任务都包含完成下一个最内任务的完成回调。这一切都在堆中。

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

    我个人觉得这种行为令人惊讶,并将其报告为错误。但是,该错误已由Microsoft以“按设计”关闭。有趣的是,JavaScript中的Promises规范(通过扩展,即await的行为)始终保证 promise 完成异步运行,而从未同步运行。这使一些JS开发人员感到困惑,但这是我所期望的行为。

    通常,它工作正常,而ExecuteSynchronously对性能的影响较小。但正如您所指出的,在某些情况下,如“异步递归”可能会导致StackOverflowException

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



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

    10-08 14:05