原文 : Stacktrace improvements in .NET Core 2.1

作者 : Ben Adams

译者 : 张很水

. NET Core 2.1 现在具有可读的异步堆栈信息!使得异步、迭代器和字典 ( key not found ) 中的堆栈更容易追踪!

这个大胆的主张意味着什么?

要知道,为了确定调用 异步 和 迭代器方法的实际重载,(这在以前)从堆栈信息中跟踪几乎是不可能的:

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) 中普遍存在堆栈难以阅读的现象

对于在 .NET 中输出异步的可阅读堆栈信息已经梦魂萦绕了5年...

我直到 2017 年 10 月才意识到这个问题,好在 .NET Core 现在是完全开源的,所以我可以改变它。

作为参考,请参阅文章底部的代码,它将会输出如下的异常堆栈:

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

有时甚至可见更详细的胶水信息:

   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()

跟踪堆栈的一般用途是确定在源代码中发生错误的位置以及对应的路径。

然而,现如今我们无法避免异步堆栈,同时还要面对很多无用的噪声(干扰)。

PR: “隐藏请求中的异常堆栈帧 ”

堆栈信息通常是从抛出异常的地方直接输出的。

当异步函数抛出异常时,它会执行一些额外的步骤来确保响应,并且在延续执行(既定方法)之前会进行清理。

当这些额外的步骤被添加到调用堆栈中时,它们不会对我们确定堆栈信息有任何帮助,因为它们实际上是在出现异常 之后 执行。

所以它们是非常嘈杂和重复的,对于确定代码在哪里出现异常上并没有任何额外的价值。

实际产生的调用堆栈和输出的不一致:

在删除这些异常堆栈帧后(隐藏请求中的异常堆栈帧 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()

并且输出的调用堆栈与实际的调用堆栈一致: 

PR: “删除异步的 Edi 边界”

异步中的异常使用 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()

PR: “处理迭代器和异步方法中的堆栈”

在上一节中,堆栈已经是干净了,但是要确定是什么情况,还是很困难的一件事。

堆栈中包含着由 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)

PR: “实现 KeyNotFoundException 的堆栈追踪”

因为有额外的奖励,我着手实现抛出 “ 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;
}
}
}

[搬运] .NET Core 2.1中改进的堆栈信息的更多相关文章

  1. delphi中获取调用堆栈信息

    异常堆栈有利于分析程序的错误,Delphi的Exception有StackTrace属性,但是值为空,因为StackTrace的信息收集Delphi委托给了第三方组件来完成,真是脑子有毛病! 借助于m ...

  2. 【译】.NET Core 3.0 中的新变化

    .NET Core 3.0 是 .NET Core 平台的下一主要版本.本文回顾了 .Net Core 发展历史,并展示了它是如何从基本支持 Web 和数据工作负载的版本 1,发展成为能够运行 Web ...

  3. .NET Core 2.1中的分层编译(预览)

    如果您是.NET性能的粉丝,最近有很多好消息,例如.NET Core 2.1中的性能改进和宣布.NET Core 2.1,但我们还有更多的好消息.分层编译是一项重要的新特性功能,我们可以作为预览供任何 ...

  4. 你所不知道的库存超限做法 服务器一般达到多少qps比较好[转] JAVA格物致知基础篇:你所不知道的返回码 深入了解EntityFramework Core 2.1延迟加载(Lazy Loading) EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public? 藏在正则表达式里的陷阱 两道面试题,带你解析Java类加载机制

    你所不知道的库存超限做法 在互联网企业中,限购的做法,多种多样,有的别出心裁,有的因循守旧,但是种种做法皆想达到的目的,无外乎几种,商品卖的完,系统抗的住,库存不超限.虽然短短数语,却有着说不完,道不 ...

  5. [转]【译】.NET Core 3.0 中的新变化

    .NET Core 3.0 是 .NET Core 平台的下一主要版本.本文回顾了 .Net Core 发展历史,并展示了它是如何从基本支持 Web 和数据工作负载的版本 1,发展成为能够运行 Web ...

  6. ASP.NET Core HTTP 管道中的那些事儿

    前言 马上2016年就要过去了,时间可是真快啊. 上次写完 Identity 系列之后,反响还不错,所以本来打算写一个 ASP.NET Core 中间件系列的,但是中间遇到了很多事情.首先是 NPOI ...

  7. 在.NET Core控制台程序中使用依赖注入

    之前都是在ASP.NET Core中使用依赖注入(Dependency Injection),昨天遇到一个场景需要在.NET Core控制台程序中使用依赖注入,由于对.NET Core中的依赖注入机制 ...

  8. .NET跨平台之旅:ASP.NET Core从传统ASP.NET的Cookie中读取用户登录信息

    在解决了asp.net core中访问memcached缓存的问题后,我们开始大踏步地向.net core进军——将更多站点向asp.net core迁移,在迁移涉及获取用户登录信息的站点时,我们遇到 ...

  9. ASP.NET Core 1.0 中的依赖项管理

    var appInsights=window.appInsights||function(config){ function r(config){t[config]=function(){var i= ...

随机推荐

  1. duilib基本流程

    duilib的基本流程如上图,通过解析一个xml文件,将文件中的内容渲染为窗口界面,这个解析过程由WindowImplBase类来完成. 基本框架如下: 1. 首先在公共头文件中加入如下内容: #in ...

  2. master_pos_wait函数与MySQL主从切换

    背景 主从切换是高可用MySQL架构的必要步骤(即使用不发生,也要有备无患).一般设置为双M(M1.M2),假设当前状态为写M1,而M2只读,切换的大致流程如下: 1.  停止应用写M1,将M1设置为 ...

  3. MYSQL:插入记录检查记录是否存在,存在则更新,不存在测插入记录SQL

    我们在开发数据库相关的逻辑过程中, 经常检查表中是否已经存在这样的一条记录, 如果存在则更新或者不做操作, 如果没有存在记录,则需要插入一条新的记录. 这样的逻辑固然可以通过两条sql语句完成. SE ...

  4. 基于 HTML5 的电力接线图 SCADA 应用

    在电力.油田燃气.供水管网等工业自动化领域 Web SCADA 的概念已经提出了多年,早些年的 Web SCADA 前端技术大部分还是基于 Flex.Silverlight 甚至 Applet 这样的 ...

  5. LVS集群之原理及概述(1)

    一. 概述 什么是集群,集群的特点.功能和分类.Linux环境下用哪些开源软件来构建一个功能强而有稳定的集群系统.了解国人内核级负载均衡开源项目linux虚拟服务器,简称LVS. 1.1 定义 集群是 ...

  6. Django普通文件上传

    前端代码: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF ...

  7. 洛谷 P1055 ISBN号码【字符串+模拟】

    P1055 ISBN号码 题目描述 每一本正式出版的图书都有一个ISBN号码与之对应,ISBN码包括9位数字.1位识别码和3位分隔符,其规定格式如“x-xxx-xxxxx-x”,其中符号“-”就是分隔 ...

  8. 洛谷 P1219 八皇后【经典DFS,温习搜索】

    P1219 八皇后 题目描述 检查一个如下的6 x 6的跳棋棋盘,有六个棋子被放置在棋盘上,使得每行.每列有且只有一个,每条对角线(包括两条主对角线的所有平行线)上至多有一个棋子. 上面的布局可以用序 ...

  9. Codeforces 626F Group Projects(滚动数组+差分dp)

    F. Group Projects time limit per test:2 seconds memory limit per test:256 megabytes input:standard i ...

  10. jquery的done和then区别

    jquery的deferred对象的done方法和then方法都能实现链式调用,但是他们的作用是有区别的,then方法中如果你传递的方法有返回值,那么他会传递给下一个链式调用的方法.而done方法与此 ...