递归和等待/异步关键字

我对await关键字的工作方式有一个脆弱的把握,我想稍微扩展一下我对它的理解。

让我头晕目眩的问题是使用递归。 这是一个例子:

 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace TestingAwaitOverflow { class Program { static void Main(string[] args) { var task = TestAsync(0); System.Threading.Thread.Sleep(100000); } static async Task TestAsync(int count) { Console.WriteLine(count); await TestAsync(count + 1); } } } 

这个显然抛出了StackOverflowException

我的理解是因为代码实际上是同步运行的,直到第一个异步操作,之后它返回一个包含异步操作信息的Task对象。 在这种情况下,没有异步操作,因此它只是在错误的承诺下继续递归,它最终会返回一个Task

现在改变它只是一点点:

 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace TestingAwaitOverflow { class Program { static void Main(string[] args) { var task = TestAsync(0); System.Threading.Thread.Sleep(100000); } static async Task TestAsync(int count) { await Task.Run(() => Console.WriteLine(count)); await TestAsync(count + 1); } } } 

这个不会抛出StackOverflowException 。 我可以理解为什么它可以工作,但我会更多地称之为直觉(它可能涉及代码如何安排使用回调以避免构建堆栈,但我无法将这种直觉转化为解释)

所以我有两个问题:

  • 第二批代码如何避免StackOverflowException
  • 第二批代码是否浪费其他资源? (例如,它是否在堆上分配了大量的荒谬的Task对象?)

谢谢!

任何函数中第一个等待的部分同步运行。 在第一种情况下,由于这种情况,它会遇到堆栈溢出 – 没有任何事情会中断调用自身的函数。

第一个await(它没有立即完成 – 对于你有很高可能性的情况)导致函数返回(并放弃它的堆栈空间!)。 它将其余部分排成一个延续。 TPL确保延续不会过于深入。 如果存在堆栈溢出的风险,则继续将排队到线程池,重置堆栈(它开始填满)。

第二个例子仍然可以溢出! 如果Task.Run任务总是立即完成怎么办? (这不太可能,但可以使用正确的OS线程调度)。 然后,异步函数永远不会被中断(导致它返回并释放所有堆栈空间),并且会产生与情况1相同的行为。

在您的第一个和第二个示例中,TestAsync仍在等待对其自身的调用返回。 区别在于递归是打印并将线程返回到第二种方法中的其他工作。 因此递归速度不足以成为堆栈溢出。 但是,第一个任务仍在等待,最终计数将达到它的最大整数大小,否则将再次抛出堆栈溢出。 重点是返回调用线程,但实际的异步方法是在同一个线程上调度的。 基本上,在等待完成之前忘记了TestAsync方法,但它仍然保存在内存中。 允许线程执行其他操作,直到等待完成,然后记住该线程并完成等待中断的位置。 额外的await调用存储线程并再次忘记它,直到await再次完成。 直到所有等待完成并且因此方法完成后,TaskAsync仍然在内存中。 所以,这就是事情。 如果我告诉方法做某事然后调用等待任务。 我的其他代码在其他地方继续运行。 等待完成后,代码会在那里重新启动并完成,然后回到它之前的那个时间。 在您的示例中,您的TaskAsync始终处于逻辑删除状态(可以这么说),直到最后一次调用完成并将调用返回到链中。

编辑:我一直说存储线程或那个线程,我的意思是例程。 它们都在同一个线程中,这是您示例中的主线程。 对不起,如果我困惑你。