C# 意外的堆栈溢出
为什么下面的异步递归会在C# 意外的堆栈溢出,c#,.net,multithreading,async-await,task-parallel-library,C#,.net,Multithreading,Async Await,Task Parallel Library,为什么下面的异步递归会在StackOverflowException中失败,为什么它恰好发生在最后一步,计数器变为零 static async Task<int> TestAsync(int c) { if (c < 0) return c; Console.WriteLine(new { c, where = "before", Environment.CurrentManagedThreadId }); await Task.Yi
StackOverflowException
中失败,为什么它恰好发生在最后一步,计数器变为零
static async Task<int> TestAsync(int c)
{
if (c < 0)
return c;
Console.WriteLine(new { c, where = "before", Environment.CurrentManagedThreadId });
await Task.Yield();
Console.WriteLine(new { c, where = "after", Environment.CurrentManagedThreadId });
return await TestAsync(c-1);
}
static void Main(string[] args)
{
Task.Run(() => TestAsync(5000)).GetAwaiter().GetResult();
}
现在,在使用TaskExt.Yield
而不是Task.Yield
时,线程每次都在翻转,但堆栈溢出仍然存在:
...
{ c = 10, where = before, CurrentManagedThreadId = 3 }
{ c = 10, where = after, CurrentManagedThreadId = 4 }
{ c = 9, where = before, CurrentManagedThreadId = 4 }
{ c = 9, where = after, CurrentManagedThreadId = 5 }
{ c = 8, where = before, CurrentManagedThreadId = 5 }
{ c = 8, where = after, CurrentManagedThreadId = 3 }
{ c = 7, where = before, CurrentManagedThreadId = 3 }
{ c = 7, where = after, CurrentManagedThreadId = 4 }
{ c = 6, where = before, CurrentManagedThreadId = 4 }
{ c = 6, where = after, CurrentManagedThreadId = 5 }
{ c = 5, where = before, CurrentManagedThreadId = 5 }
{ c = 5, where = after, CurrentManagedThreadId = 4 }
{ c = 4, where = before, CurrentManagedThreadId = 4 }
{ c = 4, where = after, CurrentManagedThreadId = 3 }
{ c = 3, where = before, CurrentManagedThreadId = 3 }
{ c = 3, where = after, CurrentManagedThreadId = 5 }
{ c = 2, where = before, CurrentManagedThreadId = 5 }
{ c = 2, where = after, CurrentManagedThreadId = 3 }
{ c = 1, where = before, CurrentManagedThreadId = 3 }
{ c = 1, where = after, CurrentManagedThreadId = 5 }
{ c = 0, where = before, CurrentManagedThreadId = 5 }
{ c = 0, where = after, CurrentManagedThreadId = 3 }
Process is terminated due to StackOverflowException.
...
{c=10,其中=before,CurrentManagedThreadId=3}
{c=10,其中=after,CurrentManagedThreadId=4}
{c=9,其中=before,CurrentManagedThreadId=4}
{c=9,其中=after,CurrentManagedThreadId=5}
{c=8,其中=before,CurrentManagedThreadId=5}
{c=8,其中=after,CurrentManagedThreadId=3}
{c=7,其中=before,CurrentManagedThreadId=3}
{c=7,其中=after,CurrentManagedThreadId=4}
{c=6,其中=before,CurrentManagedThreadId=4}
{c=6,其中=after,CurrentManagedThreadId=5}
{c=5,其中=before,CurrentManagedThreadId=5}
{c=5,其中=after,CurrentManagedThreadId=4}
{c=4,其中=before,CurrentManagedThreadId=4}
{c=4,其中=after,CurrentManagedThreadId=3}
{c=3,其中=before,CurrentManagedThreadId=3}
{c=3,其中=after,CurrentManagedThreadId=5}
{c=2,其中=before,CurrentManagedThreadId=5}
{c=2,其中=after,CurrentManagedThreadId=3}
{c=1,其中=before,CurrentManagedThreadId=3}
{c=1,其中=after,CurrentManagedThreadId=5}
{c=0,其中=before,CurrentManagedThreadId=5}
{c=0,其中=after,CurrentManagedThreadId=3}
进程因StackOverflowException而终止。
第三方物流再次进入: 请注意,堆栈溢出发生在所有迭代完成后的函数末尾。增加迭代计数不会改变这一点。将其降低到一个较小的值可以消除堆栈溢出 堆栈溢出发生在完成方法
TestAsync
的异步状态机任务时。它不会发生在“下降”上。它发生在退出并完成所有async
方法任务时
让我们首先将计数减少到2000,以减少调试器的负载。然后,查看调用堆栈:
当然是非常重复和漫长的。这是要查看的正确线程。坠机发生在:
var t = await TestAsync(c - 1);
return t;
当内部任务t
完成时,它会导致执行其余的外部TestAsync
。这只是返回语句。返回完成外部TestAsync
产生的任务。这再次触发另一个t
的完成,依此类推
TPL将一些任务延续作为性能优化。这种行为已经引起了很多悲伤,堆栈溢出问题已经证明了这一点。这一问题由来已久,迄今尚未得到任何回应。这并没有激发我们最终摆脱第三方物流再入问题的希望
TPL有一些堆栈深度检查,以在堆栈变得太深时关闭continuations的内联。这不是在这里做的原因(尚未)我不知道。请注意,堆栈上没有TaskCompletionSource
TaskAwaiter
利用TPL中的内部功能来提高性能。可能优化后的代码路径不执行堆栈深度检查。从这个意义上说,这可能是一个bug
我认为调用Yield
与问题无关,但最好将其放在这里,以确保TestAsync
的非同步完成
让我们手动编写异步状态机:
static Task<int> TestAsync(int c)
{
var tcs = new TaskCompletionSource<int>();
if (c < 0)
tcs.SetResult(0);
else
{
Task.Run(() =>
{
var t = TestAsync(c - 1);
t.ContinueWith(_ => tcs.SetResult(0), TaskContinuationOptions.ExecuteSynchronously);
});
}
return tcs.Task;
}
static void Main(string[] args)
{
Task.Run(() => TestAsync(2000).ContinueWith(_ =>
{
//breakpoint here - look at the stack
}, TaskContinuationOptions.ExecuteSynchronously)).GetAwaiter().GetResult();
}
静态任务测试同步(int c)
{
var tcs=new TaskCompletionSource();
if(c<0)
tcs.SetResult(0);
其他的
{
Task.Run(()=>
{
var t=TestAsync(c-1);
t、 ContinueWith(=>tcs.SetResult(0),TaskContinuationOptions.ExecuteSynchronously);
});
}
返回tcs.Task;
}
静态void Main(字符串[]参数)
{
Task.Run(()=>TestAsync(2000)。继续(=>
{
//断点在这里-查看堆栈
},TaskContinuationOptions.ExecuteSynchronously)).GetAwaiter().GetResult();
}
多亏了TaskContinuationOptions.ExecuteSynchronously
,我们还希望继续内联能够发生。确实如此,但不会使堆栈溢出:
这是因为TPL防止堆栈变得太深(如上所述)。完成async
方法任务时,此机制似乎不存在
如果删除了同步执行的
executes
,则堆栈很浅,不会发生内联 很高兴看到你仍然在使用匿名对象串技巧:)@usr,这是我从你那里学来的最爱之一:)一个很好的答案。事实上,这个问题的灵感来自一个客户等待者(我们称之为AlwaysAsync
),我用它来解决您在这里描述的完全相同的第三方物流问题。我在TestAsync
中使用它,但不在其返回行中使用。因此,我刚刚将返回行更改为return-await-TestAsync(c-1)。AlwaysAsync()
问题已经解决:)另一种消除堆栈跳转的方法是使用Task.Run
:async-Task-TestAsync(int-c){if(c<0)return c;return await-Task.Run(()=>TestAsync(c-1));}
@Noseratio这在实践中有效吗?我认为完成可能是一路内联的。Run内部有特殊的性能优化的展开代码。可能这里的堆栈溢出避免机制已经就位并开始工作。实际上,这似乎正在被修复,请参阅。@usr,Task.Run
无论迭代次数多少都可以工作。我自己也不知道为什么。也许您对任务的看法是正确的。Unwrap
可能包括避免内联的检查。
var t = await TestAsync(c - 1);
return t;
static Task<int> TestAsync(int c)
{
var tcs = new TaskCompletionSource<int>();
if (c < 0)
tcs.SetResult(0);
else
{
Task.Run(() =>
{
var t = TestAsync(c - 1);
t.ContinueWith(_ => tcs.SetResult(0), TaskContinuationOptions.ExecuteSynchronously);
});
}
return tcs.Task;
}
static void Main(string[] args)
{
Task.Run(() => TestAsync(2000).ContinueWith(_ =>
{
//breakpoint here - look at the stack
}, TaskContinuationOptions.ExecuteSynchronously)).GetAwaiter().GetResult();
}