一、创建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的静态分析规则,但目前还并没有给出,不过感觉并不难,有空的话我写一个试试。

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

C# 5.0 Async函数的提示和技巧的更多相关文章

  1. ES2017中的async函数

    前面的话 ES2017标准引入了 async 函数,使得异步操作变得更加方便.本文将详细介绍async函数 概述 async 函数是 Generator 函数的语法糖 使用Generator 函数,依 ...

  2. async函数解析

    转载请注明出处:async函数解析 async函数是基于Generator函数实现的,也就是说是Generator函数的语法糖.在之前的文章有介绍过Generator函数语法和异步应用,如果对其不了解 ...

  3. 如何更好的编写async函数

    2018年已经到了5月份,node的4.x版本也已经停止了维护 我司的某个服务也已经切到了8.x,目前正在做koa2.x的迁移 将之前的generator全部替换为async 但是,在替换的过程中,发 ...

  4. es6学习笔记-async函数

    1 前情摘要 前段时间时间进行项目开发,需求安排不是很合理,导致一直高强度的加班工作,这一个月不是常说的996,简直是936,还好熬过来了.在此期间不是刚学会了es6的promise,在项目有用到pr ...

  5. 17.async 函数

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

  6. nodejs记录1——async函数

    其实手动配置babel环境并不难,记录下步骤: 1.首先npm init创建一个nodejs项目 2.全局安装babel-cli处理工具:npm i babel-cli -g 3.cd到项目下安装ba ...

  7. ES6的新特性(18)——async 函数

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

  8. 浅谈async函数await用法

    今天状态不太好,睡久了懵一天. 以前只是了解过async函数,并还没有很熟练的运用过,所以先开个坑吧,以后再结合实际来更新下,可能说的有些问题希望大家指出. async和await相信大家应该不陌生, ...

  9. MySQL 创建函数失败提示1418

    MySQL 创建函数失败提示1418 在创建函数时,往往会遇到创建函数失败的情形,除去书写的创建函数的sql语句本身语法错误之外,还会碰到一个错误就是, 1418:This function has ...

随机推荐

  1. 【IOS工具类】获得设备唯一标识(兼容IOS5,6,7)

    UIDevice+IdentifierAddition.h: #import <Foundation/Foundation.h> @interface UIDevice (Identifi ...

  2. Linux Shell 函数返回值

    Shell函数返回值,常用的两种方式:return,echo 1) return 语句 shell函数的返回值,可以和其他语言的返回值一样,通过return语句返回. 示例: #!/bin/sh fu ...

  3. ruby中的模块

    什么是模块 模块(module)是Ruby特有的功能之一.类用来表现具有数据与行为(程序)的"东西", 而模块大致来说,则是只有程序部分的集合体.类与模块最大的不同在于: 1.模块 ...

  4. GitHub 优秀Android 开源项目

    阅读目录 1.Xabber客户端 2.oschina客户端 3.手机安全管家 4.星座连萌 5.玲闹铃 6.魔乐盒 7.PWP日历 8.Apollo音乐播放器 9.夏普名片识别 10.高仿人人网 11 ...

  5. MVC之Session State性能

    ASP.NET MVC之Session State性能问题(七)   前言 这一节翻译一篇有关Session State性能问题的文章,非一字一句翻译. 话题 不知道我们在真实环境中是否用到了Sess ...

  6. Net处理html页面元素工具类(HtmlAgilityPack.dll)的使用

    现在,在不少应用场合中都希望做到数据抓取,特别是基于网页部分的抓取.其实网页抓取的过程实际上是通过编程的方法,去抓取不同网站网页后,再进行 分析筛选的过程.比如,有的比较购物网站,会同时去抓取不同购物 ...

  7. 三点半们耐热i哦好家哦i囧囧【

    http://pan.baidu.com/share/link?shareid=3011665141&uk=338692646&third=15                http ...

  8. linux系统启动过程的列表

    linux系统启动过程的列表 载入BIOS的硬件信息并进行自检.然后根据设置取得第一个可启动的设备: 读取并运行第一个启动设备内MBR(master boot record,主引导分区)的boot l ...

  9. hadoop在实现kmeans算法——一个mapreduce实施

    写mapreduce程序实现kmeans算法.我们的想法可能是 1. 次迭代后的质心 2. map里.计算每一个质心与样本之间的距离,得到与样本距离最短的质心,以这个质心作为key,样本作为value ...

  10. SVN的CheckOut操作和Export操作的区别