原文 : Stacktrace improvements in .NET Core 2.1
做者 : Ben Adams
译者 : 张很水git
. NET Core 2.1 如今具备可读的异步堆栈信息!使得异步、迭代器和字典 ( key not found ) 中的堆栈更容易追踪!github
这个大胆的主张意味着什么?异步
要知道,为了肯定调用 异步 和 迭代器方法的实际重载,(这在之前)从堆栈信息中跟踪几乎是不可能的:async
System.Collections.Generic.KeyNotFoundException: The given key '0' was not present in the dictionary. at System.Collections.Generic.Dictionary`2.get_Item(TKey key) at Program.Sequence(Int32 start)+MoveNext() at Program.Sequence(Int32 start, Int32 end)+MoveNext() at Program.MethodAsync() at Program.MethodAsync(Int32 v0) at Program.MethodAsync(Int32 v0, Int32 v1) at Program.MethodAsync(Int32 v0, Int32 v1, Int32 v2) at Program.MethodAsync(Int32 v0, Int32 v1, Int32 v2, Int32 v3) at Program.Main(String[] args)
David Kean(@davkean) 于 2017 年 10 月 13 日在dotnet/corefx#24627 提出 使堆栈信息可读 的问题:函数
现在在 任务 (Task)、异步 (async) 和 等待 (await) 中广泛存在堆栈难以阅读的现象spa
对于在 .NET 中输出异步的可阅读堆栈信息已经梦魂萦绕了5年...3d
我直到 2017 年 10 月才意识到这个问题,好在 .NET Core 如今是彻底开源的,因此我能够改变它。code
做为参考,请参阅文章底部的代码,它将会输出以下的异常堆栈:blog
System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary. at System.ThrowHelper.ThrowKeyNotFoundException() at System.Collections.Generic.Dictionary`2.get_Item(TKey key) at Program.<Sequence>d__8.MoveNext() at Program.<Sequence>d__7.MoveNext() at Program.<MethodAsync>d__6.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Program.<MethodAsync>d__5.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Program.<MethodAsync>d__4.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Program.<MethodAsync>d__3.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Program.<MethodAsync>d__2.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Program.<Main>d__1.MoveNext()
(为简洁起见,删除了行号,如 in C:\Work\Exceptions\Program.cs:line 14
)ip
有时甚至可见更详细的胶水信息:
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task) at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
跟踪堆栈的通常用途是肯定在源代码中发生错误的位置以及对应的路径。
然而,现现在咱们没法避免异步堆栈,同时还要面对不少无用的噪声(干扰)。
堆栈信息一般是从抛出异常的地方直接输出的。
当异步函数抛出异常时,它会执行一些额外的步骤来确保响应,而且在延续执行(既定方法)以前会进行清理。
当这些额外的步骤被添加到调用堆栈中时,它们不会对咱们肯定堆栈信息有任何帮助,由于它们其实是在出现异常 以后 执行。
因此它们是很是嘈杂和重复的,对于肯定代码在哪里出现异常上并无任何额外的价值。
实际产生的调用堆栈和输出的不一致:
在删除这些异常堆栈帧后(隐藏请求中的异常堆栈帧
dotnet/coreclr#14652 ),跟踪堆栈开始变得平易近人:
System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary. at System.Collections.Generic.Dictionary`2.get_Item(TKey key) at Program.<Sequence>d__7.MoveNext() at Program.<Sequence>d__6.MoveNext() at Program.<MethodAsync>d__5.MoveNext() --- End of stack trace from previous location where exception was thrown --- at Program.<MethodAsync>d__4.MoveNext() --- End of stack trace from previous location where exception was thrown --- at Program.<MethodAsync>d__3.MoveNext() --- End of stack trace from previous location where exception was thrown --- at Program.<MethodAsync>d__2.MoveNext() --- End of stack trace from previous location where exception was thrown --- at Program.<MethodAsync>d__1.MoveNext() --- End of stack trace from previous location where exception was thrown --- at Program.<Main>d__0.MoveNext()
而且输出的调用堆栈与实际的调用堆栈一致:
异步中的异常使用 ExceptionDispatchInfo 类传播,这意味着着在每一个链接点都会有这样的边界信息:
--- End of stack trace from previous location where exception was thrown ---
这只是让你知道两部分调用堆栈已经合并,而且有个过渡。
它如此频繁地出如今异步中,增长了不少噪音,并无任何附加价值。
在 删除异步的 Edi 边界
dotnet/coreclr#15781 后 全部的 堆栈信息变得有价值:
System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary. at System.Collections.Generic.Dictionary`2.get_Item(TKey key) at Program.<Sequence>d__7.MoveNext() at Program.<Sequence>d__6.MoveNext() at Program.<MethodAsync>d__5.MoveNext() at Program.<MethodAsync>d__4.MoveNext() at Program.<MethodAsync>d__3.MoveNext() at Program.<MethodAsync>d__2.MoveNext() at Program.<MethodAsync>d__1.MoveNext() at Program.<Main>d__0.MoveNext()
在上一节中,堆栈已是干净了,可是要肯定是什么状况,仍是很困难的一件事。
堆栈中包含着由 C# 编译器建立的异步状态机的基础方法签名,而不只仅是(你的)源代码产生的。
你能够肯定方法的名称,可是若是不深刻挖掘,则没法肯定所调用的实际重载。
在 处理迭代器和异步方法中的堆栈
dotnet/coreclr#14655 以后,堆栈更接近原始来源:
System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary. at System.Collections.Generic.Dictionary`2.get_Item(TKey key) at Program.Sequence(Int32 start)+MoveNext() at Program.Sequence(Int32 start, Int32 end)+MoveNext() at Program.MethodAsync() at Program.MethodAsync(Int32 v0) at Program.MethodAsync(Int32 v0, Int32 v1) at Program.MethodAsync(Int32 v0, Int32 v1, Int32 v2) at Program.MethodAsync(Int32 v0, Int32 v1, Int32 v2, Int32 v3) at Program.Main(String[] args)
由于有额外的奖励,我着手实现抛出 “ KeyNotFoundException ” 的堆栈追踪。
Anirudh Agnihotry (@Anipik) 提出了 实现 KeyNotFoundException 的堆栈追踪
dotnet/coreclr#15201
这意味着这个异常如今要告诉你哪一个 key 找不到的信息:
System.Collections.Generic.KeyNotFoundException: The given key '0' was not present in the dictionary. at System.Collections.Generic.Dictionary`2.get_Item(TKey key) at Program.Sequence(Int32 start)+MoveNext() at Program.Sequence(Int32 start, Int32 end)+MoveNext() at Program.MethodAsync() at Program.MethodAsync(Int32 v0) at Program.MethodAsync(Int32 v0, Int32 v1) at Program.MethodAsync(Int32 v0, Int32 v1, Int32 v2) at Program.MethodAsync(Int32 v0, Int32 v1, Int32 v2, Int32 v3) at Program.Main(String[] args)
这些改进将在稍晚的时间发布到 Mono 上,并在下一个阶段发布。可是若是您使用的是较早的运行时版本 (.NET Core 1.0 - 2.0; .NET Framework 或 Mono) 想要得到同样的效果,须要使用 Ben.Demystifier 提供的Nuget 包,而且在你的异常中使用 .Demystify()
的方法:
catch (Exception e) { Console.WriteLine(e.Demystify()); }
这些改进将会产生与 C#相得映彰的输出信息,最使人高兴的仍是全都会被内置!
System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary. at TValue System.Collections.Generic.Dictionary<TKey, TValue>.get_Item(TKey key) at IEnumerable<int> Program.Sequence(int start)+MoveNext() at IEnumerable<int> Program.Sequence(int start, int end)+MoveNext() at async Task<int> Program.MethodAsync() at async Task<int> Program.MethodAsync(int v0) at async Task<int> Program.MethodAsync(int v0, int v1) at async Task<int> Program.MethodAsync(int v0, int v1, int v2) at async Task<int> Program.MethodAsync(int v0, int v1, int v2, int v3) at async Task Program.Main(string[] args)
.NET Core 2.1 将成为 .NET Core 的最佳版本,缘由说不完,这只是变得更美好的一小步...
class Program { static Dictionary<int, int> _dict = new Dictionary<int, int>(); static async Task Main(string[] args) { try { var value = await MethodAsync(1, 2, 3, 4); Console.WriteLine(value); } catch (Exception e) { Console.WriteLine(e); } } static async Task<int> MethodAsync(int v0, int v1, int v2, int v3) => await MethodAsync(v0, v1, v2); static async Task<int> MethodAsync(int v0, int v1, int v2) => await MethodAsync(v0, v1); static async Task<int> MethodAsync(int v0, int v1) => await MethodAsync(v0); static async Task<int> MethodAsync(int v0) => await MethodAsync(); static async Task<int> MethodAsync() { await Task.Delay(1000); int value = 0; foreach (var i in Sequence(0, 5)) { value += i; } return value; } static IEnumerable<int> Sequence(int start, int end) { for (var i = start; i <= end; i++) { foreach (var item in Sequence(i)) { yield return item; } } } static IEnumerable<int> Sequence(int start) { var end = start + 10; for (var i = start; i <= end; i++) { _dict[i] = _dict[i] + 1; // Throws exception yield return i; } } }