dotnet 警惕 async void 线程顶层异常
在应用程序设计里面,不单是 dotnet 应用程序,绝大部分都会遵循让应用在出现未处理异常状态时终结的原则。在 dotnet 应用里面,如果一个线程顶层出现未捕获异常,则应用进程将会被认为出现异常状态而退出。通常来说就是未捕获异常导致进程闪退
在 dotnet 里面,有一个隐藏的陷阱,那就是 async void 将会在没有线程同步上下文的情况下,被当成线程顶层。如果在 async void 里面发生任何未捕获的异常,严重的话将会导致进程闪退
如以下代码,在当前执行线程没有线程同步上下文的情况下,抛出的异常将会让进程闪退
async void Foo()
{
...
throw new Exception("林德熙是逗比");
}
为什么这里和线程同步上下文相关?原因是在有线程同步上下文时,执行都委托调度器执行,比如经典的线程同步上下文 WPF 主 UI 线程。这个时候主 UI 线程在 async void 里面抛出的异常是到达 Dispatcher 里,而不是线程顶层。于是可以通过全局的方式捕获异常
在 dotnet 里面,在当前 2023 没有机制可以统一捕获 async void 的异常,防止进程闪退。我在 dotnet 运行时官方仓库和大佬们讨论过这个问题,大佬的认为是当前 dotnet 的行为是符合预期和符合文档的,但我持有不同的想法,我认为这样的行为是不能做出可靠稳定的应用的,详细请看 https://github.com/dotnet/runtime/issues/76367
在 dotnet 里的另一个更加隐藏的陷阱是事件的加等里面出现异步,如以下代码
FooEvent += async (s, e) =>
{
...
throw new Exception("林德熙是逗比");
}
以上的代码里面隐式定义了 async void 方法,如此也会在当线程不在同步上下文时,抛出异常炸掉进程
解决方法是在这些 async void 方法里面自行根据业务需求,捕获异常。在大部分应用里面,一般都是应该在此捕获所有异常,除非可以无视应用进程闪退问题
以下是另外更多的行为细节
在 dotnet 里面的 async void 抛出的未捕获异常,将会进入到 AppDomain 的 UnhandledException 事件里面,然而此事件里面的 IsTerminating 属性将都是 true 且不可接住。如果进入了 UnhandledException 事件里面,还不想让进程退出,我所知道的方法只有是通过 Thread.Sleep 让当前线程不再执行,但显然这是一个很诡异的方式
在 dotnet 里面的 Task 的行为却和 async void 差异比较大,比较符合咱的认知。将 async void 改为 async Task 然后抛出未捕获异常,此时如果方法返回的 Task 没有被任何等待,将会在 Task 对象被 GC 时进入 TaskScheduler.UnobservedTaskException 事件,此事件不会导致任何的进程退出。准确来说是在 .NET Framework 4.5 开始,就不会因为 TaskScheduler.UnobservedTaskException 里的异常导致进程退出
这是因为在 Task 里面,一开始的设计也是和 async void 一样导致进程退出,然而在实际应用里面,大家都发现 Task 被等待这个事情不由实现方决定,如此导致了大量的进程退出的不可用问题,于是后面大佬就决定让 Task 里面的异常只是进入 TaskScheduler.UnobservedTaskException 事件,不会作为线程顶层异常让进程退出。另外在 .NET Framework 4.5 之后,对 Task 与线程之间的关系做了一些底层优化,导致 Task 里面执行的逻辑从逻辑上说不再属于线程顶层,这部分细节过于复杂,我自己的了解也不够就不在博客里讲了
通过本文可以了解到,在 dotnet 里面隐藏了 async void 和异步无返回值事件或委托加等逻辑里面可能出现的因为未捕获异常导致的进程闪退问题。其中的解决方法就是要么在这些代码逻辑里面捕获所有异常规避问题,要么尝试将 async void 改造为 async Task 规避问题
这里还必须着重说明的是,捕获线程顶层异常时,最好采用捕获所有异常的方式,因为可能自己的代码本来认为不会存在任何异常的逻辑,但实际运行可能遇到 OutOfMemoryException 等通用运行异常
另外在捕获异常用来记录日志的逻辑,也推荐使用双层捕获方式,解决记录异常的模块抛出的异常炸掉应用
我依然认为 async void 线程顶层异常无法统一处理导致进程退出是 dotnet 的基础设计缺陷
dotnet 警惕 async void 线程顶层异常的更多相关文章
- 《C#并发编程经典实例》学习笔记—2.9 处理 async void 方法的异常
问题 需要处理从 async void 方法传递出来的异常. 解决方案 书中建议尽量不写 async void 这样的方法,如果非写不可,建议在方法内部 try catch 所有的代码,即在方法内部处 ...
- 为什么不要使用 async void?
问题 在使用 Abp 框架的后台作业时,当后台作业抛出异常,会导致整个程序崩溃.在 Abp 框架的底层执行后台作业的时候,有 try/catch 语句块用来捕获后台任务执行时的异常,但是在这里没有生效 ...
- springboot+@async异步线程池的配置及应用
示例: 1. 配置 @EnableAsync @Configuration public class TaskExecutorConfiguration { @Autowired private Ta ...
- 为什么不要使用 Async Void ?
原文:为什么不要使用 Async Void ? 问题 在使用 Abp 框架的后台作业时,当后台作业抛出异常,会导致整个程序崩溃.在 Abp 框架的底层执行后台作业的时候,有 try/catch 语句块 ...
- WPF 线程中异常导致程序崩溃
一般我们WPF中都加全局捕获,避免出现异常导致崩溃. Application.Current.DispatcherUnhandledException += Current_DispatcherUnh ...
- 用C++11的std::async代替线程的创建
c++11中增加了线程,使得我们可以非常方便的创建线程,它的基本用法是这样的: void f(int n); std::thread t(f, n + 1); t.join(); 但是线程毕竟是属于比 ...
- (原创)用C++11的std::async代替线程的创建
c++11中增加了线程,使得我们可以非常方便的创建线程,它的基本用法是这样的: void f(int n); std::thread t(f, n + ); t.join(); 但是线程毕竟是属于比较 ...
- Java的Hook线程及捕获线程执行异常
import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.f ...
- spring boot:使用async异步线程池发送注册邮件(spring boot 2.3.1)
一,为什么要使用async异步线程池? 1,在生产环境中,有一些需要延时处理的业务场景: 例如:发送电子邮件, 给手机发短信验证码 大数据量的查询统计 远程抓取数据等 这些场景占用时间较长,而用户又没 ...
- 《C#并发编程经典实例》学习笔记—2.8 处理 async Task 方法的异常
异常处理一直是所有编程语言不可避免需要考虑的问题,C#的异步方法的异常处理和同步方法并无差别,同样要借助 try catch 语句捕获异常. 首先编写一个抛出异常的方法 static async Ta ...
随机推荐
- 记录--前端项目中运行 npm run xxx 的时候发生了什么?
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 npm 是 node 捆绑的依赖管理器,常用程度可想而知.那么你每天都在 npm/yarn run 的命令到底是如何运行项目的呢? 前端项 ...
- .Net MinimalApis响应返回值
前言 文本主要讲 MinimalApis 中的使用自定义IResultModel和系统自带IResult做响应返回值. MinimalApis支持以下类型的返回值: string - 这包括 Task ...
- Twitter的分布式自增ID算法snowflake(雪花算法) C#和Java版
概述 分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID, 但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的. 有些时候我们希望能使用一 ...
- FPGA中的时钟域问题
FPGA中的时钟域问题 一.时钟域的定义 所谓时钟域,就是同一个时钟驱动的区域.这里的驱动,是指时钟刷新D触发器的事件,体现在verilog中就是always的边沿触发信号.单一时钟域是FPGA的基本 ...
- 【mybatis踩坑】mybatis获取类型为字符串String的参数自动加引号
今天写了一个简单的测试例子,用mybatis实现新建一个MySQL数据表 整体是JavaWeb项目,下面的代码是不完整的. 这是mapper 1 <?xml version="1.0& ...
- Seaborn分布数据可视化---统计分布图
统计分布图 barplot() sns.barplot( x=None, y=None, hue=None, data=None, order=None, hue_order=None, estima ...
- C# Replace方法
例子: string tStw = "Run Status"; string tStw1 = tStw.Replace("Run Status", " ...
- 【我与openGauss的故事】如何管理数据库安全(第一部分)
前言 2021 年 6 月 10 日国家颁布数据安全法对我们国家来说具有重大意义 信息安全法 梳理几点重要意义: (一) 对数据的有效监管实现了有法可依,填补了数据安全保护立法的空白,完善了网络空间安 ...
- 比nestjs更优雅的ts控制反转策略-依赖查找
一.Cabloy5.0内测预告 Cabloy5.0采用TS对整个全栈框架进行了脱胎换骨般的大重构,并且提供了更加优雅的ts控制反转策略,让我们的业务开发更加快捷顺畅 1. 新旧技术栈对比: 后端 前端 ...
- HarmonyOS跨进程通信—IPC与RPC通信开发指导
一.IPC与RPC通信概述 基本概念 IPC(Inter-Process Communication)与RPC(Remote Procedure Call)用于实现跨进程通信,不同的是前者使用Bi ...