在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
这篇文章主要内容来自于文章C# Async Tips and Tricks Part 2 : Async Void,我本想直接翻译的,无奈由于水平有限,因此这里给的是参考原文结合自己的理解的一篇随笔。
一、创建Async函数 Async是C# 5.0中新增的关键字,通过语法糖的形式简化异步编程,它有如下三种方式:
从功能上来看方式2和方式3非常类似,都是无返回值的,区别仅仅是方式3无法等待。既然有功能更加强大的async Task的形式,为什么还要支持一个async void呢?
二、async void函数 async void函数存在的唯一目的就是和就是用于兼容现有的事件分发函数,MS在BCL库中提供了大量void类型的事件,基本形式如下: private void Button1_Click(object sender, EventArgs args) { } 这个和方式2中async Task的方法签名是不兼容的,因此,就增加了async void来实现对现有BCL库中的事件或委托兼容。 private async void Button1_Click(object sender, EventArgs args) { } 更进一步,通过ILSpy反编译异步函数可以发现,async void和async Task类型的函数的实现是不一样的,前者是AsyncVoidMethodBuilder类,而后者是 AsyncTaskMethodBuilder类。不过,它们的功能和处理方式都差不多,唯一的区别就是异常处理。
三、TPL中的未处理异常 由于async函数和TPL存在非常大的关联,在分析async函数异常处理方式前,首先来复习下TPL中对于异常是如何处理的,以如下代码为例: static void Main(string[] args) 执行上面代码后,我们可以发现:
从上面代码执行结果,我们可以大致了解Tpl对未处理异常的处理方式:
简单的说,TaskScheduler中处理了Task中抛出的异常,不会导致程序异常终止。老赵的Blog关于C#中async/await中的异常处理中详细描述了这一过程,感兴趣的朋友可以看看。 不过,Task中的未处理异常不会导致程序异常终止在另一方面也掩盖了代码中存在bug的隐患,因此建议注册TaskScheduler.UnobservedTaskException事件,对未处理异常记录日志,方便后续跟踪分析。
四、async Task函数中的未处理异常 复习完TPL的处理过程后,我们再来看看async Task中对异常处理的方式,还是前面的那个代码,只不过这次把Test函数替换成如下形式: public
static
async
Task Test() 编译这段代码的时候,会发现如下告警: 这个告警确实很有帮助,可以有效的提示忘记等待异步函数的执行完成,不过不知道为什么没有async标记的异步函数不提示这个告警。 当执行这段代码时,执行结果和前面TPL中一致:Test函数中的异常并不终止程序,异常信息在TaskScheduler.UnobservedTaskException事件中可以获取。由于AsyncTaskMethodBuilder内部就是调用Task来处理的,这个也就不难理解了(两段代码并不等价,async标记了的函数是对UI线程是特殊处理了的)。
五、async void函数中的未处理异常 下面我们再来看看async void函数未处理异常,这次我们把Test函数替换为如下形式: public
static
async void Test() 这次和上面有几点不同:
这几点主要的不同在于AsyncVoidMethodBuilder内部并没有使用TaskScheduler,线程池中的未处理异常便一直向上抛,导致程序异常终止(CLR中的处理方式可以参看Exceptions in Managed Threads这篇文章。)。 这个异常信息在桌面程序中可以通过AppDomain.UnhandledException事件查看,但该回调是没有处理异常的功能,因此一旦出现该异常,程序仍然将终止,不过可以记录个出错的原因,方便错误定位。 但是,对于WinRT程序来说就悲催了,由于不支持AppDomain,并且程序是直接crash的,都不抛个对话框挂调试器,对于那些不必现的问题连定位都不容易。在我以前的文章WinRT中的UnhandledException不能捕获异步函数的异常中就描述过这一问题。 我最初写WinRT程序的时候,为了消除CS4014告警,对于无需等待的函数,就直接写成了async void的形式,导致后续定位时欲哭不能。 因此,强烈建议严格限制async void的使用范围,尽量使用async Task来替换;对于CS4014告警,也不要无视,无需等待的任务通过下列扩展函数来清除告警。 static
class
AsyncExtension
六、Async lambda表达式 综合前面的分析,async void函数抛的异常非常难以定位,因此要严格限制使用。不过仍有一个非常隐蔽的async void类型函数非常容易被忽略,那就是async lambda表达式。 首先看一下如下代码: Enumerable.Range(0, 3).ToList().ForEach(async (i) => { throw new Exception(); }); 这段代码就隐式生成了async void函数,直接导致了程序的crash。 另外,编译器是优先生成async Task形式的匿名函数的,这一点比较好。例如对于如下两个重载函数: public
void ForEach(Action<T> action); 对于如下代码: ForEach(async i => { }); 编译器是使用ForEach(Func<T, Task> action);生成匿名函数的,而不是async void类型,但对于那些没有Func<T, Task>的重载的函数(例如前面的List.Foreach),仍然会生成async void匿名函数,需要注意。
七、编程建议: 使用async异步编程时,请注意如下事项:
总结起来一句话:async void函数能不用就不用,用的时候也要捕获所有异常再用。 不过,这个做起来还是有些难度的,有的时候会在不经意间写了async void函数(例如在lambda表达式中),并且不容易发现。最好还是需要一个工具来分析下程序集,检查函数是否只用于UI Event回调。原文作者说会提供一个基于这个原则的fxcop的静态分析规则,但目前还并没有给出,不过感觉并不难,有空的话我写一个试试。 最后,该作者的这系列文章一共有三篇,写的都非常不错,这里强烈推荐下: 我这篇随笔主要是基于第二篇文章写的,如果有空的话剩下两篇放在一篇随笔中一并介绍下。 |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论