在应用程序设计里面,不单是 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 线程顶层异常的更多相关文章

  1. 《C#并发编程经典实例》学习笔记—2.9 处理 async void 方法的异常

    问题 需要处理从 async void 方法传递出来的异常. 解决方案 书中建议尽量不写 async void 这样的方法,如果非写不可,建议在方法内部 try catch 所有的代码,即在方法内部处 ...

  2. 为什么不要使用 async void?

    问题 在使用 Abp 框架的后台作业时,当后台作业抛出异常,会导致整个程序崩溃.在 Abp 框架的底层执行后台作业的时候,有 try/catch 语句块用来捕获后台任务执行时的异常,但是在这里没有生效 ...

  3. springboot+@async异步线程池的配置及应用

    示例: 1. 配置 @EnableAsync @Configuration public class TaskExecutorConfiguration { @Autowired private Ta ...

  4. 为什么不要使用 Async Void ?

    原文:为什么不要使用 Async Void ? 问题 在使用 Abp 框架的后台作业时,当后台作业抛出异常,会导致整个程序崩溃.在 Abp 框架的底层执行后台作业的时候,有 try/catch 语句块 ...

  5. WPF 线程中异常导致程序崩溃

    一般我们WPF中都加全局捕获,避免出现异常导致崩溃. Application.Current.DispatcherUnhandledException += Current_DispatcherUnh ...

  6. 用C++11的std::async代替线程的创建

    c++11中增加了线程,使得我们可以非常方便的创建线程,它的基本用法是这样的: void f(int n); std::thread t(f, n + 1); t.join(); 但是线程毕竟是属于比 ...

  7. (原创)用C++11的std::async代替线程的创建

    c++11中增加了线程,使得我们可以非常方便的创建线程,它的基本用法是这样的: void f(int n); std::thread t(f, n + ); t.join(); 但是线程毕竟是属于比较 ...

  8. Java的Hook线程及捕获线程执行异常

    import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.f ...

  9. spring boot:使用async异步线程池发送注册邮件(spring boot 2.3.1)

    一,为什么要使用async异步线程池? 1,在生产环境中,有一些需要延时处理的业务场景: 例如:发送电子邮件, 给手机发短信验证码 大数据量的查询统计 远程抓取数据等 这些场景占用时间较长,而用户又没 ...

  10. 《C#并发编程经典实例》学习笔记—2.8 处理 async Task 方法的异常

    异常处理一直是所有编程语言不可避免需要考虑的问题,C#的异步方法的异常处理和同步方法并无差别,同样要借助 try catch 语句捕获异常. 首先编写一个抛出异常的方法 static async Ta ...

随机推荐

  1. View事件机制分析

    目录介绍 01.Android中事件分发顺序 1.1 事件分发的对象是谁 1.2 事件分发的本质 1.3 事件在哪些对象间进行传递 1.4 事件分发过程涉及方法 1.5 Android中事件分发顺序 ...

  2. WinAppSDK / WinUI3 项目无法使用 SystemEvents 的问题

    SystemEvents 是一个开发 win32 窗口项目很常用的类,其中封装了一些常用的系统广播消息.在 WinUI3 项目中,SystemEvents 事件经常无法触发,简单排查了一下原因. Sy ...

  3. 香港Azure/.NET俱乐部第一次聚会纪实 - WPF在金融业的商业价值

    香港Azure/.NET俱乐部第一次聚会于2019年12月29日在香港上环地铁站星巴克举行. 香港Azure/.NET俱乐部的定位是:以商业价值为导向. 基于这个定位,可以推导出如下准则: 面向大型企 ...

  4. echarts中实现多个label

    先来个效果图 如果你刚好需要实现这种效果,那么可以瞅一瞅了 我要开始水文了 如图所示,图中顶部部分文字乍一看好像是独立于柱状图,显示在最顶上,其实它也是由柱状图模拟而成. 只是吧图形相关属性做了隐藏, ...

  5. 添加AvalonEdit控件到WinForm

    public frmTest() { InitializeComponent(); ElementHost host = new ElementHost(); host.Size = new Size ...

  6. cyc_to_led

    Entity: cyc_to_led File: cyc_to_led.v Diagram Generics Generic name Type Value Description MD_SIM_AB ...

  7. java使用Ffmpeg合成音频和视频

    1.Maven依赖 <!-- 需要注意,javacv主要是一组API为主,还需要加入对应的实现 --> <dependency> <groupId>org.byte ...

  8. KingbaseES 集群运维系列 -- 验证系统用户修改密码或密码过期对ssh互信的影响

    案例说明: Kingbase V8主备流复制集群在通用机环境部署和运维,需要建立主机间的ssh互信,如果ssh互信被破坏,将导致集群故障.但有的生产环境为了系统安全需要,会配置密码管理策略,定期的修改 ...

  9. Jetty的threadpool模块

    Jetty提供的线程池相关的模块,如下: threadpool threadpool-virtual,使用JDK 21提供的virtual threads. threadpool-virtual-pr ...

  10. C++ 中的可移植性和跨平台开发

    在当今软件开发行业中,跨平台开发已经成为了一种非常流行的方式.C++作为一门强大的编程语言,也被广泛应用于跨平台开发中.然而,由于不同操作系统的差异和限制,C++在不同的平台上的表现可能会有所不同.为 ...