Ins*_*yro 5 c# recursion async-await
为什么下面的代码在 .Net4.8 中导致 StackOverflowException 只有 17 深度递归?但是这在 NetCore 3.1 中不会发生(我可以将计数设置为 10_000 并且它仍然有效)
class Program
{
static async Task Main(string[] args)
{
try
{
await TestAsync(17);
}
catch(Exception e)
{
Console.WriteLine("Exception caught: " + e);
}
}
static async Task TestAsync(int count)
{
await Task.Run(() =>
{
if (count <= 0)
throw new Exception("ex");
});
Console.WriteLine(count);
await TestAsync2(count);
}
static async Task TestAsync2(int count) => await TestAsync3(count);
static async Task TestAsync3(int count) => await TestAsync4(count);
static async Task TestAsync4(int count) => await TestAsync5(count);
static async Task TestAsync5(int count) => await TestAsync6(count);
static async Task TestAsync6(int count) => await TestAsync(count - 1);
}
Run Code Online (Sandbox Code Playgroud)
这是 .Net 4.8 中的已知错误吗?我会在这样的函数中排除超过 17 个级别的递归......这是否实际上意味着不推荐使用 async/await 编写递归?
更新:简化版
class Program
{
// needs to be compiled as AnyCpu Prefer 64-bit
static async Task Main(string[] args)
{
try
{
await TestAsync(97); // 96 still works
}
catch(Exception e)
{
Console.WriteLine("Exception caught: " + e);
}
}
static async Task TestAsync(int count)
{
await Task.Run(() =>
{
if (count <= 0)
throw new Exception("ex");
});
Console.WriteLine(count);
await TestAsync(count-1);
}
}
Run Code Online (Sandbox Code Playgroud)
在选择Any Cpu with Prefer 32-bit disabled 时,它只会发生得如此之快,但可在多个 .net 版本(.Net 4.7.2 和 .Net 4.8)上的多台机器(Windows 1903 和 1909)上重现
我怀疑您在完成时看到了堆栈溢出- 即,每个数字都一直打印到1堆栈溢出消息之前。
我的猜测是,这种行为是因为await使用了同步延续。应该有代码可以防止同步延续溢出堆栈,但它是启发式的,并不总是有效。
我怀疑这种行为不会发生在 .NET Core 上,因为大量优化工作已经投入到 .NET Core 的async支持中,这可能意味着该平台上的延续占用更少的堆栈空间,从而使启发式检查发挥作用。启发式本身也可能已在 .NET Core 中修复。无论哪种方式,我都不会屏息期待 .NET Framework 获得这些更新。
我会在这样的函数中排除超过 17 级的递归......
不是真的 17。你有 102 级递归 ( 17 * 6)。要测量实际占用的堆栈空间,它将是17 * 6 * (number of stacks to resume continuations)。在我的机器上,17 个有效;它在超过 200 个地方失败(1200 个深度调用)。
请记住,这只发生在尾递归异步函数的长序列上 - 即,它们中没有一个在其await. 如果您将任何函数更改为在递归之后await进行一些其他异步工作,这将避免堆栈溢出:
static async Task TestAsync(int count)
{
await Task.Run(() =>
{
if (count <= 0)
throw new Exception("ex");
});
Console.WriteLine(count);
try
{
await TestAsync2(count);
}
finally
{
await Task.Yield(); // some other async work
}
}
Run Code Online (Sandbox Code Playgroud)