【发布时间】:2015-12-01 11:03:00
【问题描述】:
为什么下面的异步递归会以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();
}
输出:
... { c = 10,其中 = 之前,CurrentManagedThreadId = 4 } { c = 10,其中 = 之后,CurrentManagedThreadId = 4 } { c = 9, where = before, CurrentManagedThreadId = 4 } { c = 9,其中 = 之后,CurrentManagedThreadId = 5 } { c = 8, where = before, CurrentManagedThreadId = 5 } { c = 8, where = after, CurrentManagedThreadId = 5 } { c = 7,其中 = 之前,CurrentManagedThreadId = 5 } { c = 7,其中 = 之后,CurrentManagedThreadId = 5 } { c = 6, where = before, CurrentManagedThreadId = 5 } { c = 6, where = after, CurrentManagedThreadId = 5 } { c = 5, where = before, CurrentManagedThreadId = 5 } { c = 5,其中 = 之后,CurrentManagedThreadId = 5 } { c = 4, where = before, CurrentManagedThreadId = 5 } { c = 4, where = after, CurrentManagedThreadId = 5 } { c = 3,其中 = 之前,CurrentManagedThreadId = 5 } { c = 3,其中 = 之后,CurrentManagedThreadId = 5 } { c = 2, where = before, CurrentManagedThreadId = 5 } { c = 2,其中 = 之后,CurrentManagedThreadId = 5 } { c = 1,其中 = 之前,CurrentManagedThreadId = 5 } { c = 1,其中 = 之后,CurrentManagedThreadId = 5 } { c = 0,其中 = 之前,CurrentManagedThreadId = 5 } { c = 0,其中 = 之后,CurrentManagedThreadId = 5 } 进程因 StackOverflowException 而终止。我在安装 .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 时,线程每次都在翻转,但堆栈溢出仍然存在:
【问题讨论】:
-
很高兴看到您仍在使用匿名对象 ToString 技巧 :)
-
@usr,自从我向你学习以来,它一直是我的最爱之一 :)
标签: c# .net multithreading async-await task-parallel-library