async/await 和 TaskCompletionSource 的奇怪堆栈跟踪增长
Weird stack trace growth with async/await and TaskCompletionSource
以下 C# 代码:
class Program
{
static readonly List<TaskCompletionSource<bool>> buffer =
new List<TaskCompletionSource<bool>>();
static Timer timer;
public static void Main()
{
var outstanding = Enumerable.Range(1, 10)
.Select(Enqueue)
.ToArray();
timer = new Timer(x => Flush(), null,
TimeSpan.FromSeconds(1),
TimeSpan.FromMilliseconds(-1));
try
{
Task.WaitAll(outstanding);
}
catch {}
Console.ReadKey();
}
static Task Enqueue(int i)
{
var task = new TaskCompletionSource<bool>();
buffer.Add(task);
return task.Task;
}
static void Flush()
{
try
{
throw new ArgumentException("test");
}
catch (Exception e)
{
foreach (var each in buffer)
{
var lenBefore = e.StackTrace.Length;
each.TrySetException(e);
var lenAfter = e.StackTrace.Length;
Console.WriteLine($"Before - After: {lenBefore} - {lenAfter}");
Console.WriteLine(e.StackTrace);
}
}
}
}
产生:
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
但是当我将 Enqueue
方法更改为异步时:
static async Task Enqueue(int i)
{
var task = new TaskCompletionSource<bool>();
buffer.Add(task);
return await task.Task;
}
结果是:
Before - After: 149 - 643
Before - After: 643 - 1137
Before - After: 1137 - 1631
Before - After: 1631 - 2125
Before - After: 2125 - 2619
Before - After: 2619 - 3113
Before - After: 3113 - 3607
Before - After: 3607 - 4101
Before - After: 4101 - 4595
Before - After: 4595 - 5089
看起来每个缓冲项目的堆栈跟踪递归增长。对于第一项异常堆栈跟踪将是:
at Program.Flush() in C:\src\Program.cs:line 41
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotificati...
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
而第二个将如下所示,依此类推:
at Program.Flush() in C:\src\Program.cs:line 41
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotificati...
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotificati...
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
这是怎么回事,如何解决?
简短回答:await
尝试解包结果,而方法 withod await
不尝试访问任务结果。
更长的答案:
- 调用堆栈的重复部分如下所示:
- ValidateEnd method of
TaskAwaiter
is being inlined, and the HandleNonSuccessAndDebuggerNotification causes a call to ThrowForNonSuccess which seems to be inlined too and, as single exception is used to set result for 10 TaskCompletionSource
s, the reason of growing stack of that exception can be seen here.
简单的解决方案是在每次 TrySetException
调用时使用 new Exception("Some descriptive message", originalException)
。
问题是您正在为每个任务重复使用相同的异常,因此它将所有堆栈附加在一起,假设这是它们进行的顺序。
如果您改为为每个
创建一个新的例外
var ex = new Exception(e.Message, e);
each.TrySetException(ex);
然后你会得到
Before - After: 87 - 341
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
Before - After: 87 - 341
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
Before - After: 87 - 341
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
Before - After: 87 - 341
如果您还使用 Ben.Demystifier 库
var ex = new Exception(e.Message, e);
each.TrySetException(ex);
var lenAfter = ex.Demystify().StackTrace.Length;
然后会进一步下降:
Before - After: 87 - 105
at async Task Program.Enqueue(int i) in C:\src\Program.cs:line 34
Before - After: 92 - 105
at async Task Program.Enqueue(int i) in C:\src\Program.cs:line 34
Before - After: 92 - 105
at async Task Program.Enqueue(int i) in C:\src\Program.cs:line 34
Before - After: 92 - 105
以下 C# 代码:
class Program
{
static readonly List<TaskCompletionSource<bool>> buffer =
new List<TaskCompletionSource<bool>>();
static Timer timer;
public static void Main()
{
var outstanding = Enumerable.Range(1, 10)
.Select(Enqueue)
.ToArray();
timer = new Timer(x => Flush(), null,
TimeSpan.FromSeconds(1),
TimeSpan.FromMilliseconds(-1));
try
{
Task.WaitAll(outstanding);
}
catch {}
Console.ReadKey();
}
static Task Enqueue(int i)
{
var task = new TaskCompletionSource<bool>();
buffer.Add(task);
return task.Task;
}
static void Flush()
{
try
{
throw new ArgumentException("test");
}
catch (Exception e)
{
foreach (var each in buffer)
{
var lenBefore = e.StackTrace.Length;
each.TrySetException(e);
var lenAfter = e.StackTrace.Length;
Console.WriteLine($"Before - After: {lenBefore} - {lenAfter}");
Console.WriteLine(e.StackTrace);
}
}
}
}
产生:
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
但是当我将 Enqueue
方法更改为异步时:
static async Task Enqueue(int i)
{
var task = new TaskCompletionSource<bool>();
buffer.Add(task);
return await task.Task;
}
结果是:
Before - After: 149 - 643
Before - After: 643 - 1137
Before - After: 1137 - 1631
Before - After: 1631 - 2125
Before - After: 2125 - 2619
Before - After: 2619 - 3113
Before - After: 3113 - 3607
Before - After: 3607 - 4101
Before - After: 4101 - 4595
Before - After: 4595 - 5089
看起来每个缓冲项目的堆栈跟踪递归增长。对于第一项异常堆栈跟踪将是:
at Program.Flush() in C:\src\Program.cs:line 41
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotificati...
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
而第二个将如下所示,依此类推:
at Program.Flush() in C:\src\Program.cs:line 41
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotificati...
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotificati...
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
这是怎么回事,如何解决?
简短回答:await
尝试解包结果,而方法 withod await
不尝试访问任务结果。
更长的答案:
- 调用堆栈的重复部分如下所示:
- ValidateEnd method of
TaskAwaiter
is being inlined, and the HandleNonSuccessAndDebuggerNotification causes a call to ThrowForNonSuccess which seems to be inlined too and, as single exception is used to set result for 10TaskCompletionSource
s, the reason of growing stack of that exception can be seen here.
简单的解决方案是在每次 TrySetException
调用时使用 new Exception("Some descriptive message", originalException)
。
问题是您正在为每个任务重复使用相同的异常,因此它将所有堆栈附加在一起,假设这是它们进行的顺序。
如果您改为为每个
创建一个新的例外var ex = new Exception(e.Message, e);
each.TrySetException(ex);
然后你会得到
Before - After: 87 - 341
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
Before - After: 87 - 341
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
Before - After: 87 - 341
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
Before - After: 87 - 341
如果您还使用 Ben.Demystifier 库
var ex = new Exception(e.Message, e);
each.TrySetException(ex);
var lenAfter = ex.Demystify().StackTrace.Length;
然后会进一步下降:
Before - After: 87 - 105
at async Task Program.Enqueue(int i) in C:\src\Program.cs:line 34
Before - After: 92 - 105
at async Task Program.Enqueue(int i) in C:\src\Program.cs:line 34
Before - After: 92 - 105
at async Task Program.Enqueue(int i) in C:\src\Program.cs:line 34
Before - After: 92 - 105