一、创建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. Windows移动开发(四)——闭关修炼

    非常久不写博客了,不是由于不想写,仅仅是近期公司任务比較多,最终十一有时间出来冒泡了. 今天继续介绍移动开发中的重中之重--内存管理. C#代码是托管代码,C# 程序猿非常少像C/CPP程序猿那样为程 ...

  2. SPOJ DISUBSTR(后缀数组)

    传送门:DISUBSTR 题意:给定一个字符串,求不相同的子串. 分析:对于每个sa[i]贡献n-a[i]个后缀,然后减去a[i]与a[i-1]的公共前缀height[i],则每个a[i]贡献n-sa ...

  3. 移动App測试实战:顶级互联网企业软件測试和质量提升最佳实践

    这篇是计算机类的优质预售推荐>>>><移动App測试实战:顶级互联网企业软件測试和质量提升最佳实践> 国内顶级互联网公司測试实战经验总结.阿里.腾讯.京东.携程.百 ...

  4. [WebGL入门]十,矩阵计算和外部库

    注:文章译自http://wgld.org/,原作者杉本雅広(doxas),文章中假设有我的额外说明,我会加上[lufy:],另外,鄙人webgl研究还不够深入,一些专业词语,假设翻译有误,欢迎大家指 ...

  5. cocos2d-x 3.0游戏实例学习笔记《卡牌塔防》第八部---怪物出场

    /* 说明: **1.本次游戏实例是<cocos2d-x游戏开发之旅>上的最后一个游戏,这里用3.0重写并做下笔记 **2.我也问过木头本人啦,他说:随便写.第一别全然照搬代码.第二能够说 ...

  6. 智能指针 shared_ptr 解析

    近期正在进行<Effective C++>的第二遍阅读,书里面多个条款涉及到了shared_ptr智能指针,介绍的太分散,学习起来麻烦.写篇blog整理一下. LinJM   @HQU s ...

  7. 命令含执行JAVA程序

    1.当类没有包名时 javac Test.java java Test   2.当有包名情况下 package com.me.Test; javac -d . Test.java java com.m ...

  8. Windows编程之非模态对话框

    1  创建非模态对话框 <1>  HWNDCreateDialog(  HINSTANCE hInstance,  // handle to module LPCTSTRlpTemplat ...

  9. 将行政区域导入SQL SERVER

    步骤如下: 一.到国家统计局网站,找到县及县以上行政区划页面. 我找到的是这个:http://www.stats.gov.cn/tjbz/xzqhdm/t20130118_402867249.htm ...

  10. Swift 的类、结构体、枚举等的构造过程Initialization(下)

    类的继承和构造过程 类里面的全部存储型属性--包含全部继承自父类的属性--都必须在构造过程中设置初始值. Swift 提供了两种类型的类构造器来确保全部类实例中存储型属性都能获得初始值,它们各自是指定 ...