我对 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
的错误 promise 下不断递归。
现在稍微改变一下:
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
。我可以sortof 明白它为什么有效,但我更愿意称之为直觉(它可能涉及如何安排代码以使用回调来避免构建堆栈,但我无法翻译那种直觉变成了一种解释)
所以我有两个问题:
- 第二批代码如何避免
StackOverflowException
? - 第二批代码是否浪费了其他资源? (例如,它是否在堆上分配了大量的 Task 对象?)
谢谢!
最佳答案
任何函数中直到第一个 await 的部分都是同步运行的。在第一种情况下,它会因此遇到堆栈溢出 - 没有任何东西会中断调用自身的函数。
第一个 await(不会立即完成 - 这很可能是您的情况)导致函数返回(并放弃其堆栈空间!)。它将其余部分作为延续进行排队。 TPL 确保延续永远不会嵌套得太深。如果存在堆栈溢出的风险,继续将排队到线程池,重置堆栈(开始填满)。
第二个示例仍然会溢出!如果 Task.Run
任务总是立即完成怎么办? (这不太可能,但通过正确的操作系统线程调度是可能的)。然后,异步函数将永远不会被中断(导致它返回并释放所有堆栈空间),并且会产生与情况 1 相同的行为。
关于c# - 递归和 await/async 关键字,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13808166/