[C#] StackOverflowExceptions在堆栈展开的嵌套异步方法中


Answers

Question

我们有很多嵌套的异步方法,看到我们并不真正理解的行为。 以这个简单的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

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

e 331 of 2000 (on threadId: 4)

当我们改变参数

AsyncStackSample.exe 2000 true

我们以这个消息结束

e 831 of 2000 (on threadId: 4)

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

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

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