C# 5.0 Async函数的提示和技巧

这篇文章主要内容来自于文章C# Async Tips and Tricks Part 2 : Async Void,我本想直接翻译的,无奈由于水平有限,因此这里给的是参考原文结合自己的理解的一篇随笔。

一、创建Async函数

Async是C# 5.0中新增的关键字,通过语法糖的形式简化异步编程,它有如下三种方式:

  1. async Task<T> MyReturningMethod { return default(T); }
  2. async Task MyMethod() { }
  3. async void MyFireAndForgetMethod() { }

从功能上来看方式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)     {         TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;         Test();
        Console.ReadLine();        //等待Test任务执行完成         GC.Collect();         GC.WaitForPendingFinalizers();
        Console.ReadLine();     }
    static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)     {         Console.WriteLine(e.Exception);     }
    public static Task Test()     {         return Task.Run(() => { throw new Exception(); });     }

执行上面代码后,我们可以发现:

  1. Test任务抛异常后,程序能仍然能正常运行
  2. GC执行时,可以通过TaskScheduler.UnobservedTaskException捕获到Test任务抛异常的信息。

从上面代码执行结果,我们可以大致了解Tpl对未处理异常的处理方式:

  1. Task未处理异常不会继续往上抛导致程序异常终止
  2. Task中未处理异常可以通过TaskScheduler.UnobservedTaskException事件捕获
  3. TaskScheduler.UnobservedTaskException事件并不是在抛异常时立即的,而是GC时从Finalizer线程里触发并执行的。

简单的说,TaskScheduler中处理了Task中抛出的异常,不会导致程序异常终止。老赵的Blog关于C#中async/await中的异常处理中详细描述了这一过程,感兴趣的朋友可以看看。

不过,Task中的未处理异常不会导致程序异常终止在另一方面也掩盖了代码中存在bug的隐患,因此建议注册TaskScheduler.UnobservedTaskException事件,对未处理异常记录日志,方便后续跟踪分析。

四、async Task函数中的未处理异常

复习完TPL的处理过程后,我们再来看看async Task中对异常处理的方式,还是前面的那个代码,只不过这次把Test函数替换成如下形式:

public static async Task Test()     {         throw new Exception();     }

编译这段代码的时候,会发现如下告警: warning CS4014: 由于不等待此调用,因此会在此调用完成前继续执行当前方法。请考虑向此调用的结果应用"await"运算符。

这个告警确实很有帮助,可以有效的提示忘记等待异步函数的执行完成,不过不知道为什么没有async标记的异步函数不提示这个告警。

当执行这段代码时,执行结果和前面TPL中一致:Test函数中的异常并不终止程序,异常信息在TaskScheduler.UnobservedTaskException事件中可以获取。由于AsyncTaskMethodBuilder内部就是调用Task来处理的,这个也就不难理解了(两段代码并不等价,async标记了的函数是对UI线程是特殊处理了的)。

五、async void函数中的未处理异常

下面我们再来看看async void函数未处理异常,这次我们把Test函数替换为如下形式:

public static async void Test()     {         throw new Exception();     }

这次和上面有几点不同:

  1. 编译的时候没有CS4014告警
  2. Test函数执行时抛异常直接终止了程序
  3. 在TaskScheduler.UnobservedTaskException中查看不到异常信息

这几点主要的不同在于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     {         public static void IgnorCompletion(this Task task)         {         }     }

六、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);     public void ForEach(Func<T, Task> action);

对于如下代码:

ForEach(async i => { });

编译器是使用ForEach(Func<T, Task> action);生成匿名函数的,而不是async void类型,但对于那些没有Func<T, Task>的重载的函数(例如前面的List.Foreach),仍然会生成async void匿名函数,需要注意。

七、编程建议:

使用async异步编程时,请注意如下事项:

  1. async void函数只能在UI Event回调中使用。
  2. async void函数中一定要用try-catch捕获所有异常,否则会很容易导致程序崩溃。
  3. async void类型的lambda表达式非常隐蔽,并且容易在无意中编写出来,尤其需要注意。
  4. 不要忽视CS4014告警,更不要为了消除CS4014告警而改用async void函数。 确实无需等待的async Task函数用我前面写的扩展函数IgnorCompletion消除这个告警。
  5. 注册TaskScheduler.UnobservedTaskException事件,记录Task中未处理异常信息,方便分析及错误定位。(注意,这个回调里面不能进行耗时操作,具体原因参看前面的老赵的那篇Blog)

总结起来一句话:async void函数能不用就不用,用的时候也要捕获所有异常再用。

不过,这个做起来还是有些难度的,有的时候会在不经意间写了async void函数(例如在lambda表达式中),并且不容易发现。最好还是需要一个工具来分析下程序集,检查函数是否只用于UI Event回调。原文作者说会提供一个基于这个原则的fxcop的静态分析规则,但目前还并没有给出,不过感觉并不难,有空的话我写一个试试。

最后,该作者的这系列文章一共有三篇,写的都非常不错,这里强烈推荐下:

  1. C# 5.0 Async Tips and Tricks, Part 1
  2. C# Async Tips and Tricks Part 2 : Async Void
  3. C# Async Tips and Tricks, Part 3: Tasks and the Synchronization Context

说明:这篇随笔主要是基于第二篇文章写的(详见下一篇关于5.0异步的内容)

转载于:https://www.cnblogs.com/changbaishan/p/4624396.html

C# 5.0 Async函数的提示和技巧相关推荐

  1. 小哥哥小姐姐,来尝尝 Async 函数这块语法糖

    编者注:众所周知,JS 最大的特性就是异步,异步提高了性能但是却给我们编写带来了一定困难,造就了令人发指的回调地狱.为了解决这个问题,一个又一个的解决方案被提出来.今天我们请来了 <JavaSc ...

  2. 阮一峰老师的ES6入门:async 函数

    async 函数 1. 含义 ES2017 标准引入了 async 函数,使得异步操作变得更加方便. async 函数是什么?一句话,它就是 Generator 函数的语法糖. 前文有一个 Gener ...

  3. ES6学习笔记(十六)async函数

    1.含义 ES2017 标准引入了 async 函数,使得异步操作变得更加方便. async 函数是什么?一句话,它就是 Generator 函数的语法糖,号称异步的终极解决方案. 前文有一个 Gen ...

  4. “睡服”面试官系列第十九篇之async函数(建议收藏学习)

    目录 1. 含义 2. 基本用法 3. 语法 3.1返回 Promise 对象 3.2Promise 对象的状态变化 3.3await 命令 3.4错误处理 3.5使用注意点 4. async 函数的 ...

  5. es6 async函数的语法

    async函数的语法 async函数的语法规则总体上比较简单,难点是错误处理机制. 返回 Promise 对象 async函数返回一个 Promise 对象. async函数内部return语句返回的 ...

  6. fetch结合(async函数来使用)

    fetch结合(async函数来使用) <ul id="students"> </ul> ajax请求 function ajax(url){let xml ...

  7. ES6 async函数(超级详细、易懂)

    下面是对 ES6 async函数的整理,希望可以帮助到有需要的小伙伴~ 文章目录 async函数是什么 异步函数声明式 异步函数表达式 返回Promise对象 await表达式 await处理错误 a ...

  8. 浅谈async函数await用法

    async和await相信大家应该不陌生,让异步处理变得更友好. 其实这玩意儿就是个Generator的语法糖,想深入学习得去看看Generator,不然你可能只停留在会用的阶段. 用法很简单,看代码 ...

  9. async 函数 ajax,Async 函数的使用及简单实现

    解决回调地狱的异步操作,Async 函数是终极办法,但了解生成器和 Promise 有助于理解 Async 函数原理.由于内容较多,分三部分进行,这是第三部分,介绍 Async 函数相关.第一部分介绍 ...

  10. JS async 函数

    async 函数 含义 ES2017 标准引入了 async 函数,使得异步操作变得更加方便. async 函数是什么?一句话,它就是 Generator 函数的语法糖. 生成器 function* ...

最新文章

  1. python语言只采用解释一种翻译方式对吗_python-guide翻译
  2. 绩效面谈流程,阿里是这样做的
  3. 数码之下 - 一道选择题讨论采样定理
  4. Oracle sys或者system的默认密码
  5. Java ClassLoader getPackage()方法与示例
  6. 2015年,大数据将改变客户关系
  7. windows查看usb信息命令_ADB命令你知道多少?ADB冻结系统应用?
  8. markdown 本地链接_markdown多平台发布及七牛图床使用
  9. 小米5手机刷成开发版获取root权限
  10. PDCA理念融入软件测试
  11. CentOS安装NPM
  12. 常用遥感SIF和GPP数据集
  13. tangent space /handness
  14. 看完这篇电磁兼容分层与综合设计法,EMC你还不懂就没救了
  15. Android 硬件传感器
  16. 青蛙游戏 linux,小青蛙2048
  17. SF21 | 利用PSY指标,我们来开发一个短线模型?
  18. SAP MM模块简单介绍
  19. Excel的SUMPRODUCT函数及其用法
  20. firewall火墙策略

热门文章

  1. Python学习-logging
  2. linux增加虚拟内存
  3. vs 中程序被锁定的问题
  4. video标签:以视频为背景的网页
  5. APACHE ACTIVEMQ安装
  6. rhel6 dhcp dns配置小贴士
  7. 远程控制软件用户群分析
  8. php ci的session和php session,php及codeigniter使用session-cookie的方法(详解)
  9. maven的安装步骤
  10. Redis 6.0 源码阅读笔记(13 ) -- Redis 集群节点选举流程