StackOverflowExceptions在堆栈展开时的嵌套异步方法中

我们有很多嵌套的异步方法,并查看我们不太了解的行为。 以例如这个简单的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()来解决这个问题,但我们有几个问题:

  • 为什么堆栈在展开路径上增长(与不引起await线程切换的方法相比)?
  • 为什么StackOverflowException在Exception的情况下比在我们不抛出异常的时候更早出现?

  • 为什么堆栈在展开路径上增长(与不引起await线程切换的方法相比)?

    核心原因是因为await使用TaskContinuationOptions.ExecuteSynchronously标志来调度其延续。

    因此,当执行“最内层” Yield ,最终会产生3000个不完整的任务,每个“内部”任务持有完成回调,完成下一个内部任务。 这一切都在堆。

    当最内层Yield恢复时(在线程池线程上),continuation(同步)执行Test方法的剩余部分,完成其任务,同步执行Test方法的剩余部分,完成其任务等。 ,几千次。 所以,每个任务完成后,该线程池线程上的调用堆栈实际上正在增长。

    就我个人而言,我觉得这种行为令人惊讶,并将其报告为一个错误。 但是,这个错误被微软封为“按设计”。 有趣的是,JavaScript中的Promises规范(以及扩展, await的行为)总是有承诺完成异步运行并且从不同步运行。 这让一些JS开发人员感到困惑,但这是我期望的行为。

    通常情况下,它可以正常工作,并且ExecuteSynchronously作为次要的性能改进。 但是正如你所说的那样,有些情况像“异步递归”可能导致StackOverflowException

    如果堆栈太满,BCL中有一些启发式异步运行延续,但它们只是启发式,并不总是有效。

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

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

    链接地址: http://www.djcxy.com/p/80767.html

    上一篇: StackOverflowExceptions in nested async methods on unwinding of the stack

    下一篇: ShimsContext.Create is throwing a StackOverflowException