尽管产生了意外的堆栈溢出
Unexpected stack overflow despite yielding
为什么以下异步递归失败并显示 WhosebugException
,为什么它正好发生在最后一步,当计数器变为零时?
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();
}
输出:
...
{ c = 10, where = before, CurrentManagedThreadId = 4 }
{ 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 = 5 }
{ c = 7, where = before, CurrentManagedThreadId = 5 }
{ c = 7, where = after, CurrentManagedThreadId = 5 }
{ c = 6, where = before, CurrentManagedThreadId = 5 }
{ c = 6, where = after, CurrentManagedThreadId = 5 }
{ c = 5, where = before, CurrentManagedThreadId = 5 }
{ c = 5, where = after, CurrentManagedThreadId = 5 }
{ c = 4, where = before, CurrentManagedThreadId = 5 }
{ c = 4, where = after, CurrentManagedThreadId = 5 }
{ c = 3, where = before, CurrentManagedThreadId = 5 }
{ c = 3, where = after, CurrentManagedThreadId = 5 }
{ c = 2, where = before, CurrentManagedThreadId = 5 }
{ c = 2, where = after, CurrentManagedThreadId = 5 }
{ c = 1, where = before, CurrentManagedThreadId = 5 }
{ c = 1, where = after, CurrentManagedThreadId = 5 }
{ c = 0, where = before, CurrentManagedThreadId = 5 }
{ c = 0, where = after, CurrentManagedThreadId = 5 }
Process is terminated due to WhosebugException.
我在安装 .NET 4.6 时看到了这一点。该项目是一个针对 .NET 4.5 的控制台应用程序。
我知道 Task.Yield
的延续可能会由 ThreadPool.QueueUserWorkItem
在同一个线程上安排(如上面的#5),以防线程已经被释放到池中 - 紧随其后await Task.Yield()
,但在实际安排 QueueUserWorkItem
回调之前。
但是我不明白堆栈为什么和在哪里仍在加深。延续不应该发生在同一个堆栈帧上,即使它是在同一个线程上调用的。
我更进一步,实现了 Yield
的自定义版本,确保不会在同一线程上继续:
public static class TaskExt
{
public static YieldAwaiter Yield() { return new YieldAwaiter(); }
public struct YieldAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion
{
public YieldAwaiter GetAwaiter() { return this; }
public bool IsCompleted { get { return false; } }
public void GetResult() { }
public void UnsafeOnCompleted(Action continuation)
{
using (var mre = new ManualResetEvent(initialState: false))
{
ThreadPool.UnsafeQueueUserWorkItem(_ =>
{
mre.Set();
continuation();
}, null);
mre.WaitOne();
}
}
public void OnCompleted(Action continuation)
{
throw new NotImplementedException();
}
}
}
现在,当使用 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 WhosebugException.
TPL 重入再次来袭:
请注意,堆栈溢出发生在函数结束时 在 完成所有迭代之后。增加迭代次数不会改变这一点。将其降低到少量可消除堆栈溢出。
完成方法TestAsync
的异步状态机任务时发生堆栈溢出。它不会发生在 "descend" 上。它发生在退出并完成所有 async
方法任务时。
让我们首先将计数减少到 2000 以减少调试器的负载。然后,查看调用栈:
当然非常重复和冗长。这是要查看的正确线程。崩溃发生在:
var t = await TestAsync(c - 1);
return t;
当内部任务 t
完成时,它会导致执行外部 TestAsync
的其余部分。这只是 return 语句。 return 完成了外部 TestAsync
产生的任务。这再次触发另一个 t
的完成,依此类推。
TPL 内联一些任务延续作为性能优化。正如 Stack Overflow 问题所证明的那样,这种行为已经引起了很多悲伤。 It has been requested to remove it. 这个问题很老了,目前还没有收到任何回复。这并没有激发我们最终摆脱 TPL 重入问题的希望。
TPL 有一些堆栈深度检查以在堆栈变得太深时关闭延续的内联。由于我(尚)不知道的原因,这里没有这样做。请注意,堆栈上没有任何地方有 TaskCompletionSource
。 TaskAwaiter
利用 TPL 中的内部函数来提高性能。也许优化的代码路径不执行堆栈深度检查。从这个意义上说,这可能是一个错误。
我不认为调用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();
}
多亏了 TaskContinuationOptions.ExecuteSynchronously
,我们还期待继续内联的发生。确实如此,但不会溢出堆栈:
那是因为 TPL 防止堆栈变得太深(如上所述)。完成 async
方法任务时似乎不存在此机制。
如果 ExecuteSynchronously
被移除,那么堆栈是浅的并且没有内联发生。
为什么以下异步递归失败并显示 WhosebugException
,为什么它正好发生在最后一步,当计数器变为零时?
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();
}
输出:
... { c = 10, where = before, CurrentManagedThreadId = 4 } { 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 = 5 } { c = 7, where = before, CurrentManagedThreadId = 5 } { c = 7, where = after, CurrentManagedThreadId = 5 } { c = 6, where = before, CurrentManagedThreadId = 5 } { c = 6, where = after, CurrentManagedThreadId = 5 } { c = 5, where = before, CurrentManagedThreadId = 5 } { c = 5, where = after, CurrentManagedThreadId = 5 } { c = 4, where = before, CurrentManagedThreadId = 5 } { c = 4, where = after, CurrentManagedThreadId = 5 } { c = 3, where = before, CurrentManagedThreadId = 5 } { c = 3, where = after, CurrentManagedThreadId = 5 } { c = 2, where = before, CurrentManagedThreadId = 5 } { c = 2, where = after, CurrentManagedThreadId = 5 } { c = 1, where = before, CurrentManagedThreadId = 5 } { c = 1, where = after, CurrentManagedThreadId = 5 } { c = 0, where = before, CurrentManagedThreadId = 5 } { c = 0, where = after, CurrentManagedThreadId = 5 } Process is terminated due to WhosebugException.
我在安装 .NET 4.6 时看到了这一点。该项目是一个针对 .NET 4.5 的控制台应用程序。
我知道 Task.Yield
的延续可能会由 ThreadPool.QueueUserWorkItem
在同一个线程上安排(如上面的#5),以防线程已经被释放到池中 - 紧随其后await Task.Yield()
,但在实际安排 QueueUserWorkItem
回调之前。
但是我不明白堆栈为什么和在哪里仍在加深。延续不应该发生在同一个堆栈帧上,即使它是在同一个线程上调用的。
我更进一步,实现了 Yield
的自定义版本,确保不会在同一线程上继续:
public static class TaskExt
{
public static YieldAwaiter Yield() { return new YieldAwaiter(); }
public struct YieldAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion
{
public YieldAwaiter GetAwaiter() { return this; }
public bool IsCompleted { get { return false; } }
public void GetResult() { }
public void UnsafeOnCompleted(Action continuation)
{
using (var mre = new ManualResetEvent(initialState: false))
{
ThreadPool.UnsafeQueueUserWorkItem(_ =>
{
mre.Set();
continuation();
}, null);
mre.WaitOne();
}
}
public void OnCompleted(Action continuation)
{
throw new NotImplementedException();
}
}
}
现在,当使用 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 WhosebugException.
TPL 重入再次来袭:
请注意,堆栈溢出发生在函数结束时 在 完成所有迭代之后。增加迭代次数不会改变这一点。将其降低到少量可消除堆栈溢出。
完成方法TestAsync
的异步状态机任务时发生堆栈溢出。它不会发生在 "descend" 上。它发生在退出并完成所有 async
方法任务时。
让我们首先将计数减少到 2000 以减少调试器的负载。然后,查看调用栈:
当然非常重复和冗长。这是要查看的正确线程。崩溃发生在:
var t = await TestAsync(c - 1);
return t;
当内部任务 t
完成时,它会导致执行外部 TestAsync
的其余部分。这只是 return 语句。 return 完成了外部 TestAsync
产生的任务。这再次触发另一个 t
的完成,依此类推。
TPL 内联一些任务延续作为性能优化。正如 Stack Overflow 问题所证明的那样,这种行为已经引起了很多悲伤。 It has been requested to remove it. 这个问题很老了,目前还没有收到任何回复。这并没有激发我们最终摆脱 TPL 重入问题的希望。
TPL 有一些堆栈深度检查以在堆栈变得太深时关闭延续的内联。由于我(尚)不知道的原因,这里没有这样做。请注意,堆栈上没有任何地方有 TaskCompletionSource
。 TaskAwaiter
利用 TPL 中的内部函数来提高性能。也许优化的代码路径不执行堆栈深度检查。从这个意义上说,这可能是一个错误。
我不认为调用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();
}
多亏了 TaskContinuationOptions.ExecuteSynchronously
,我们还期待继续内联的发生。确实如此,但不会溢出堆栈:
那是因为 TPL 防止堆栈变得太深(如上所述)。完成 async
方法任务时似乎不存在此机制。
如果 ExecuteSynchronously
被移除,那么堆栈是浅的并且没有内联发生。