我对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对象吗?)
谢谢!
Task
对象的函数替换了Task.Run()
,这会重新引发堆栈溢出异常吗?(或者我刚刚提出了一些不可能的东西?) - riwalkawait Task.FromResult<object>(null);
。这就是为什么你应该避免在短时间内多次调用await
的单个函数;你应该确保等待的任务要么操作时间相当长,要么任务数量有限,以便同步运行它们是可以的。 - ServyTask.Yield()
(这保证了需要发布一个 continuation),则可以确保不会出现堆栈溢出的问题(但要付出性能代价)。请注意,Yield
返回的是一个除Task
之外的可等待对象。对于任务来说,要获得这种保证更加困难,因为你必须确保在 await 特性查询时它没有完成。 - usr