我们有很多嵌套的异步方法,并且看到了我们并不真正了解的行为。以这个简单的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
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,但它们只是试探法,并不总是有效。
这是个好问题。我不知道。 :)