在同步编程中,一旦出现错误就会抛出异常,我们可以使用try…catch来捕捉异常,而未被捕获的异常则会不断向上传递,形成一个简单而统一的错误处理机制。不过对于异步编程来说,异常处理一直是件麻烦的事情,这也是C#中async/await或是Jscex等异步编程模型的优势之一。但是,同步的错误处理机制,并不能完全避免异步形式的错误处理方式,这需要一定实践规范来保证,至少我们需要了解async/await到底是如何捕获和分发异常的。在开发Jscex的过程中,我也在C#内部邮件邮件列表中了解了很多关于TPL和C#异步特性的问题,错误处理也是其中之一。在此记录一下吧。

使用try…catch捕获异常


首先我们来看下这段代码:

static async Task ThrowAfter(int timeout, Exception ex)
{
await Task.Delay(timeout);
throw ex;
} static void PrintException(Exception ex)
{
Console.WriteLine("Time: {0}\n{1}\n============", _watch.Elapsed, ex);
} static Stopwatch _watch = new Stopwatch(); static async Task MissHandling()
{
var t1 = ThrowAfter(, new NotSupportedException("Error 1"));
var t2 = ThrowAfter(, new NotImplementedException("Error 2")); try
{
await t1;
}
catch (NotSupportedException ex)
{
PrintException(ex);
}
} static void Main(string[] args)
{
_watch.Start(); MissHandling(); Console.ReadLine();
}

这段代码的输出如下:

Time: 00:00:01.2058970
System.NotSupportedException: Error 1
at AsyncErrorHandling.Program.d__0.MoveNext() in ...\Program.cs:line 16
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at AsyncErrorHandling.Program.d__3.MoveNext() in ...\Program.cs:line 33
============

在MissingHandling方法中,我们首先使用ThrowAfter方法开启两个任务,它们会分别在一秒及两秒后抛出两个不同的异常。但是在接下来的try中,我们只对t1进行await操作。很容易理解,t1抛出的NotSupportedException将被catch捕获,耗时大约为1秒左右——当然,从上面的数据可以看出,其实t1在被“捕获”时已经耗费了1.2时间,误差较大。这是因为程序刚启动,TPL内部正处于“热身”状态,在调度上会有较大开销。这里反倒是另一个问题倒更值得关注:t2在两秒后抛出的NotImplementedException到哪里去了?

未捕获的异常


C#的async/await功能基于TPL的Task对象,每个await操作符都是“等待”一个Task完成。在之前(或者说如今)的TPL中,Task对象的析构函数会查看它的Exception对象有没有被“访问”过,如果没有,且Task对象出现了异常,则会抛出这个异常,最终导致的结果往往便是进程退出。因此,我们必须小心翼翼地处理每一个Task对象的错误,不得遗漏。在.NET 4.5中这个行为被改变了,对于任何没有被检查过的异常,便会触发TaskSchedular.UnobservedTaskException事件——如果您不监听这个事件,未捕获的异常也就这么无影无踪了。

为此,我们对Main方法进行一个简单的改造。

static void Main(string[] args)
{
TaskScheduler.UnobservedTaskException += (_, ev) => PrintException(ev.Exception); _watch.Start(); MissHandling(); while (true)
{
Thread.Sleep();
GC.Collect();
}
}

改造有两点,一是响应TaskScheduler.UnobservedTaskException,这自然不必多说。还有一点便是不断地触发垃圾回收,以便Finalizer线程调用析构函数。如今这段代码除了打印出之前的信息之外,还会输出以下内容:

Time: 00:00:03.0984560
System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. ---> System.NotImplementedException: Error 2
at AsyncErrorHandling.Program.d__0.MoveNext() in ...\Program.cs:line 16
--- End of inner exception stack trace ---
---> (Inner Exception #0) System.NotImplementedException: Error 2
at AsyncErrorHandling.Program.d__0.MoveNext() in ...\Program.cs:line 16<---
============

从上面的信息中可以看出,UnobservedTaskException事件并非在“抛出”异常后便立即触发,而是在某次垃圾收集过程,从Finalizer线程里触发并执行。从中也不难得出这样的结论:便是该事件的响应方法不能过于耗时,更加不能阻塞,否则便会对程序性能造成灾难性的影响。

那么假如我们要同时处理t1和t2中抛出的异常该怎么做呢?此时便是Task.WhenAll方法上场的时候了:

static async Task BothHandled()
{
var t1 = ThrowAfter(, new NotSupportedException("Error 1"));
var t2 = ThrowAfter(, new NotImplementedException("Error 2")); try
{
await Task.WhenAll(t1, t2);
}
catch (NotSupportedException ex)
{
PrintException(ex);
}
}

如果您执行这段代码,会发现其输出与第一段代码相同,但其实不同的是,第一段代码中t2的异常被“遗漏”了,而目前这段代码t1和t2的异常都被捕获了,只不过await语句仅仅“抛出”了“其中一个”异常而已。

WhenAll是一个辅助方法,它的输入是n个Task对象,输出则是个返回它们的结果数组的Task对象。新的Task对象会在所有输入全部“结束”后才完成。在这里“结束”的意思包括成功和失败(取消也是失败的一种,即抛出了OperationCanceledException)。换句话说,假如这n个输入中的某个Task对象很快便失败了,也必须等待其他所有输入对象成功或是失败之后,新的Task对象才算完成。而新的Task对象完成后又可能会有两种表现:

  • 所有输入Task对象都成功了:则返回它们的结果数组。
  • 至少一个输入Task对象失败了:则抛出“其中一个”异常。

全部成功的情况自不必说,那么在失败的情况下,什么叫做抛出“其中一个”异常?如果我们要处理所有抛出的异常该怎么办?下次我们继续讨论这方面的问题。

相关文章


关于C#中async/await中的异常处理(上)
关于C#中async/await中的异常处理(下)

原文链接

关于C#中async/await中的异常处理(上)-(转载)的更多相关文章

  1. 关于C#中async/await中的异常处理(上)

    关于C#中async/await中的异常处理(上) 2012-04-11 09:15 by 老赵, 17919 visits 在同步编程中,一旦出现错误就会抛出异常,我们可以使用try…catch来捕 ...

  2. 关于C#中async/await中的异常处理(下)-(转载)

    上一篇文章里我们讨论了某些async/await的用法中出现遗漏异常的情况,并且谈到该如何使用WhenAll辅助方法来避免这种情况.WhenAll辅助方法将会汇总一系列的任务对象,一旦其中某个出错,则 ...

  3. C#中async/await中的异常处理

    在同步编程中,一旦出现错误就会抛出异常,我们可以使用try-catch来捕捉异常,而未被捕获的异常则会不断向上传递,形成一个简单而统一的错误处理机制.不过对于异步编程来说,异常处理一直是件麻烦的事情, ...

  4. [译]async/await中阻塞死锁

    这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的两篇博文中翻译过来. 原文1:Don'tBlock o ...

  5. [译]async/await中使用阻塞式代码导致死锁 百万数据排序:优化的选择排序(堆排序)

    [译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的 ...

  6. [翻译] Python 3.5中async/await的工作机制

    Python 3.5中async/await的工作机制 多处翻译出于自己理解,如有疑惑请参考原文 原文链接 身为Python核心开发组的成员,我对于这门语言的各种细节充满好奇.尽管我很清楚自己不可能对 ...

  7. [译]async/await中使用阻塞式代码导致死锁

    原文:[译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Clea ...

  8. .NET Core学习笔记(3)——async/await中的Exception处理

    在写了很多年.NET程序之后,年长的猿类在面对异步编程时,仍不时会犯下致命错误,乃至被拖出去杀了祭天.本篇就async/await中的Exception处理进行讨论,为种族的繁衍生息做出贡献……处理a ...

  9. .NET异步操作学习之一:Async/Await中异常的处理

    以前的异常处理,习惯了过程式的把出现的异常全部捕捉一遍,然后再进行处理.Async/Await关键字出来之后的确简化了异步编程,但也带来了一些问题.接下来自己将对这对关键字进行学习.然后把研究结果放在 ...

随机推荐

  1. ES6——TDZ(暂时性死区)

    暂时性的死区(Temporal Dead Zone),简写为 TDZ: 只要块级作用域里存在let命令,它所声明的变量就绑定这个区域,不在受外部的影响 let 和 const 声明的变量不会被提升到作 ...

  2. 走通Django的基本流程

    工程目录及文件的说明 manage.py:一个命令行工具,可以使我们用多种方式对Django项目进行交互 __init__.py:一个空文件,它告诉Python这个文件的上级目录应该看做一个pytho ...

  3. JS--我发现,原来你是这样的JS(二)(基础概念--躯壳篇--不妨从中文角度看js)

    一.介绍 这是红宝书(JavaScript高级程序设计 3版)的读书笔记第二篇(基础概念--躯壳篇),有着部分第三章的知识内容,当然其中还有我个人的理解. 红宝书这本书可以说是难啃的,要看完不容易,挺 ...

  4. Nginx的程序架构优化

    解耦是开发人员中流行的有个名词,简单的说就是把一堆的程序代码按照业务用途分开,然后提供服务,例如:注册登录.上传.下载.浏览列表.商品内容页面.订单支付等都应该是独立的程序服务,只不过在客户端看来是一 ...

  5. SQLServer 事物与索引

    SqlServer 事物与索引 分享by:授客 QQ:1033553122 详情点击百度网盘分享链接: SqlServer 事物与索引.ppt

  6. onlyoffice新版5.1.2版解决中文汉字输入重复等问题

    一个星期前新版更新,从5.07到了5.1.2.9.所以我的网盘中的镜像也做了相应的更新. 原来说的几个缺点,已经修改了几个,比如chrome浏览器和opera浏览器中,输入汉字,然后输入符号的时候,会 ...

  7. autocad视图汇报,像ppt那样汇报

    在大部分场景中,工程师可以用cad进行汇报,避免去做PPT,浪费时间,ppt一般都是用一次就丢弃.而工程师对于设计的中间汇报,或三维bim汇报,评审汇报,展示汇报等,都可以直接用cad软件,方法是用视 ...

  8. LeetCode题解之Merge k Sorted Lists 解法二

    1.题目描述 2.分析 利用 vector 存储指针,同时合并k个链表. 3.代码 ListNode* mergeKLists(vector<ListNode*>& lists) ...

  9. C# WinForm中添加用户控件

    转:https://blog.csdn.net/haelang/article/details/40681003 有的时候我们需要频繁使用一些系统默认工具的组合,那么就可以使用自定义用户控件. 起一个 ...

  10. mysql InnoDB引擎索引超过长度限制

    组合索引长度之和大于 767 bytes并无影响,当有某个字段定义长度大于 767 bytes(1000*3)时,仅产生告警,但不影响创建,超长字段会取前 255 字符作为前缀索引,并且组合索引中字段 ...