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

Mic*_*eem 15 .net c# stack-overflow async-await

我们有很多嵌套的异步方法,看到我们并不真正理解的行为.以这个简单的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";
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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

AsyncStackSample.exe 2000 false
Run Code Online (Sandbox Code Playgroud)

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

e 331 of 2000 (on threadId: 4)
Run Code Online (Sandbox Code Playgroud)

当我们将参数更改为

AsyncStackSample.exe 2000 true
Run Code Online (Sandbox Code Playgroud)

我们以此消息结束

e 831 of 2000 (on threadId: 4)
Run Code Online (Sandbox Code Playgroud)

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

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

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

Ste*_*ary 13

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

核心原因是因为await使用TaskContinuationOptions.ExecuteSynchronously标志安排其延续.

因此,当执行"最内层"时Yield,你最终得到的是3000个未完成的任务,每个"内部"任务都有一个完成回调,完成最后一个内部任务.这一切都在堆中.

当最内层Yield恢复时(在线程池线程上),continuation(同步)执行Test方法的其余部分,完成其任务,(同步)执行Test方法的其余部分,完成任务等等,一些一千次.因此,该线程池线程上的调用堆栈实际上随着每个任务的完成而增长.

就个人而言,我发现这种行为令人惊讶并将其报告为一个错误.但是,该漏洞被微软称为"按设计".值得注意的是,JavaScript中的Promises规范(以及扩展中的行为await)始终具有承诺完成异步运行且从不同步.这让一些JS开发者感到困惑,但这是我所期望的行为.

通常情况下,它可以正常运行,并且ExecuteSynchronously可以作为次要的性能改进.但正如您所指出的,有些情况可能会导致"异步递归" StackOverflowException.

目前一些在BCL异步运行延续堆栈是太满启发,但他们只是试探,并不总是工作.

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

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

  • 你可以把它作为一个单独的问题.这种方式非常具体 - 只是一个问题. (2认同)