C#中async/await中的异常处理
在同步编程中,一旦出现错误就会抛出异常,我们可以使用try…catch来捕捉异常,而未被捕获的异常则会不断向上传递,形成一个简单而统一的错误处理机制。不过对于异步编程来说,异常处理一直是件麻烦的事情,这也是C#中async/await或是Jscex等异步编程模型的优势之一。但是,同步的错误处理机制,并不能完全避免异步形式的错误处理方式,这需要一定实践规范来保证,至少我们需要了解async/await到底是如何捕获和分发异常的。在开发Jscex的过程中,我深入了解了很多关于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: ::01.2058970
System.NotSupportedException: Error
at AsyncErrorHandling.Program.d__0.MoveNext() in ...\Program.cs:line
--- 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
============
在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: ::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
--- End of inner exception stack trace ---
---> (Inner Exception #) System.NotImplementedException: Error
at AsyncErrorHandling.Program.d__0.MoveNext() in ...\Program.cs:line <---
============
从上面的信息中可以看出,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对象失败了:则抛出“其中一个”异常。
全部成功的情况自不必说,那么在失败的情况下,什么叫做抛出“其中一个”异常?如果我们要处理所有抛出的异常该怎么办?接着我们来详细讨论await操作在异常分派时的相关行为。
await抛出异常时的行为
要理解await的行为,还是从理解Task对象的异常表现开始。Task对象有一个Exception属性,类型为AggregateException,在执行成功的情况下该属性返回null,否则便包含了“所有”出错的对象。既然是AggregateException,则意为着可能包含多个子异常,这种情况往往会在任务的父子关系中出现,具体情况可以参考MSDN中的相关说明。在许多情况下一个Task内部只会出现一个异常,此时这个AggregateException的InnerExceptions属性自然也就只一个元素。
Task对象本身还有一个Wait方法,它会阻塞当前执行代码,直到任务完成。在出现异常的时候,它会将自身的AggregateException抛出:
try
{
t.Wait();
}
catch (AggregateException ex)
{
...
}
Wait方法是“真阻塞”,而await操作则是使用阻塞语义的代码实现非阻塞的效果,这个区别一定要分清。与Wait方法不同的是,await操作符效果并非是“抛出”Task对象上的Exception属性,而只是抛出这个AggregateException对象上的“其中一个”元素。我向C#开发组询问这么做的设计考虑,他们回答道,这个决策在内部也经历了激烈的争论,最终的选择这种方式而不是直接抛出Task对象上的AggregateException是为了避免编写出冗余的代码,并让代码与传统同步编程习惯更为接近。
他们举了一个简单的示例,假如一个Task对象t可能抛出两种异常,现在的错误捕获方式为:
try
{
await t1;
}
catch (NotSupportedException ex)
{
...
}
catch (NotImplementedException ex)
{
...
}
catch (Exception ex)
{
...
}
假如await操作抛出的是AggregateException,那么代码就必须写为:
try
{
await t1;
}
catch (AggregateException ex)
{
var innerEx = ex.InnerExceptions[]; if (innerEx is NotSupportedException)
{
...
}
else if (innerEx is NotImplementedException)
{
...
}
else
{
...
}
}
显然前者更贴近传统的同步编程习惯。但是问题在于,如果这个Task中包含了多个异常怎么办?之前的描述是抛出“其中一个”异常,对于开发者来说,“其中一个”这种模糊的说法自然无法令人满意,但事实的确如此。从内部邮件列表中的讨论来看,C#开发团队提到他们“故意”不提供文档说明究竟会抛出哪个异常,因为他们并不想做出这方面的约束,因为这部分行为一旦写入文档,便成为一个规定和限制,为了类库的兼容性今后也无法对此做出修改。
他们也提到,如果单论目前的实现,await操作会从Task.Exception.InnerExceptions集合中挑出第一个异常,并对外“抛出”,这是System.Runtime.CompilerServices.TaskAwaiter类中定义的行为。但是既然这并非是“文档化”的固定行为,开发人员也尽量不要依赖这点。
WhenAll的异常汇总方式
其实这个话题跟async/await的行为没有任何联系,WhenAll返回的是普通的Task对象,TaskAwaiter也丝毫不关心当前等待的Task对象是否来自于WhenAll,不过既然WhenAll是最常用的辅助方法之一,也顺便将其讲清楚吧。
WhenAll得到Task对象,其结果是用数组存放的所有子Task的结果,而在出现异常时,其Exception属性返回的AggregateException集合会包含所有子Task中抛出的异常。请注意,每个子Task中抛出的异常将会存放在它自身的AggregateException集合中,WhenAll返回的Task对象将会“按顺序”收集各个AggregateException集合中的元素,而并非收集每个AggregateException对象。
我们使用一个简单的例子来理解这点:
Task all = null;
try
{
await (all = Task.WhenAll(
Task.WhenAll(
ThrowAfter(, new Exception("Ex3")),
ThrowAfter(, new Exception("Ex1"))),
ThrowAfter(, new Exception("Ex2"))));
}
catch (Exception ex)
{
...
}
这段代码使用了嵌套的WhenAll方法,总共会出现三个异常,按其抛出的时机排序,其顺序为Ex1,Ex2及Ex3。那么请问:
- catch语句捕获的异常是哪个?
- all.Exception这个AggregateException集合中异常按顺序是哪些?
结果如下:
- catch语句捕获的异常是Ex3,因为它是all.Exception这个AggregateException集合中的第一个元素,但还是请牢记这点,这只是当前TaskAwaiter所实现的行为,而并非是由文档规定的结果。
- all.Exception这个AggregateException集合中异常有三个,按顺序是Ex3,Ex1和Ex2。WhenAll得到的Task对象,是根据输入的Task对象顺序来决定自身AggreagteException集合中异常对象的存放顺序。这个顺序跟异常的抛出时机没有任何关系。
这里我们也顺便可以得知,如果您不想捕获AggregateException集合中的“其中一个”异常,而是想处理所有异常的话,也可以写这样的代码:
Task all = null;
try
{
await (all = Task.WhenAll(
ThrowAfter(, new Exception("Ex1")),
ThrowAfter(, new Exception("Ex2"))));
}
catch
{
foreach (var ex in all.Exception.InnerExceptions)
{
...
}
}
当然,这里使用Task.WhenAll作为示例,是因为这个Task对象可以明确包含多个异常,但并非只有Task.WhenAll返回的Task对象才可能包含多个异常,例如Task对象在创建时指定了父子关系,也会让父任务里包含各个子任务里出现的异常。
假如异常未被捕获
最后再来看一个简单的问题,我们一直在关注一个async方法中“捕获”异常的行为,假如异常没有成功捕获,直接对外抛出的时候,对任务本身的有什么影响呢?且看这个示例:
static async Task SomeTask()
{
try
{
await Task.WhenAll(
ThrowAfter(, new NotSupportedException("Ex1")),
ThrowAfter(, new NotImplementedException("Ex2")));
}
catch (NotImplementedException) { }
} static void Main(string[] args)
{
_watch.Start(); SomeTask().ContinueWith(t => PrintException(t.Exception)); Console.ReadLine();
}
这段代码的输出结果是:
System.AggregateException: One or more errors occurred. ---> System.NotSupportedException: Ex1
at AsyncErrorHandling.Program.d__0.MoveNext() in ...\Program.cs:line
--- 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
--- End of inner exception stack trace ---
---> (Inner Exception #) System.NotSupportedException: Ex1
at AsyncErrorHandling.Program.d__0.MoveNext() in ...\Program.cs:line
--- 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 <---
AggregateException的打印内容不那么容易读,我们可以关注它Inner Exception #0这样的信息。从时间上说,Ex2先于Ex1抛出,而catch的目标是NotImplementedException。但从之前的描述我们可以知道,WhenAll返回的Task内部的异常集合,与各异常抛出的时机没有关系,因此await操作符抛出的是Ex1,是NotSupportedException,而它不会被catch到,因此SomeTask返回的Task对象也会包含这个异常——也仅仅是抛出这个异常,而Ex2对于外部就不可见了。
如果您想在外部处理所有的异常,则可以这样:
Task all = null;
try
{
await (all = Task.WhenAll(
ThrowAfter(, new NotSupportedException("Ex1")),
ThrowAfter(, new NotImplementedException("Ex2"))));
}
catch
{
throw all.Exception;
}
此时打印的结果便是一个AggregateException包含着另一个AggregateException,其中包含了Ex1和Ex2。为了“解开”这种嵌套关系,AggregateException也提供了一个Flatten方法,可以将这种嵌套完全“铺平”,例如:
SomeTask().ContinueWith(t => PrintException(t.Exception.Flatten()));
此时打印的结果便直接是一个AggregateException包含着Ex1与Ex2了。
原文: https://msdn.microsoft.com/zh-cn/library/jj619227.aspx
C#中async/await中的异常处理的更多相关文章
- 关于C#中async/await中的异常处理(上)-(转载)
在同步编程中,一旦出现错误就会抛出异常,我们可以使用try…catch来捕捉异常,而未被捕获的异常则会不断向上传递,形成一个简单而统一的错误处理机制.不过对于异步编程来说,异常处理一直是件麻烦的事情, ...
- 关于C#中async/await中的异常处理(上)
关于C#中async/await中的异常处理(上) 2012-04-11 09:15 by 老赵, 17919 visits 在同步编程中,一旦出现错误就会抛出异常,我们可以使用try…catch来捕 ...
- 关于C#中async/await中的异常处理(下)-(转载)
上一篇文章里我们讨论了某些async/await的用法中出现遗漏异常的情况,并且谈到该如何使用WhenAll辅助方法来避免这种情况.WhenAll辅助方法将会汇总一系列的任务对象,一旦其中某个出错,则 ...
- [译]async/await中阻塞死锁
这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的两篇博文中翻译过来. 原文1:Don'tBlock o ...
- [译]async/await中使用阻塞式代码导致死锁 百万数据排序:优化的选择排序(堆排序)
[译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的 ...
- [翻译] Python 3.5中async/await的工作机制
Python 3.5中async/await的工作机制 多处翻译出于自己理解,如有疑惑请参考原文 原文链接 身为Python核心开发组的成员,我对于这门语言的各种细节充满好奇.尽管我很清楚自己不可能对 ...
- [译]async/await中使用阻塞式代码导致死锁
原文:[译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Clea ...
- .NET Core学习笔记(3)——async/await中的Exception处理
在写了很多年.NET程序之后,年长的猿类在面对异步编程时,仍不时会犯下致命错误,乃至被拖出去杀了祭天.本篇就async/await中的Exception处理进行讨论,为种族的繁衍生息做出贡献……处理a ...
- .NET异步操作学习之一:Async/Await中异常的处理
以前的异常处理,习惯了过程式的把出现的异常全部捕捉一遍,然后再进行处理.Async/Await关键字出来之后的确简化了异步编程,但也带来了一些问题.接下来自己将对这对关键字进行学习.然后把研究结果放在 ...
随机推荐
- UESTC 886 方老师金币堆 --合并石子DP
环状合并石子问题. 环状无非是第n个要和第1个相邻.可以复制该行石子到原来那行的右边即可达到目的. 定义:dp[i][j]代表从第i堆合并至第j堆所要消耗的最小体力. 转移方程:dp[i][j]=mi ...
- 云盘WEB资料下载链接
入门三板斧:http://www.cnblogs.com/jikey/p/3613082.html 入门看这个:http://pan.baidu.com/s/1pJqJvAV 入门JS视频:http: ...
- nginx的学习材料
1. 章亦春 关于nginx的讲解 http://agentzh.org/misc/slides/nginx-conf-scripting/nginx-conf-scripting.html#2 2. ...
- 转: __asm__ __volatile__内嵌汇编用法简述
from: http://www.embedu.org/Column/Column28.htm __asm__ __volatile__内嵌汇编用法简述 作者:刘老师,华清远见嵌入式学院高级讲师,AR ...
- css继承性和不继承的属性。
在CSS中并不是所有的属性都是能够继承的,因此在使用时一定要了解哪些是能够继承的哪些是不能够继承的.visibility和cursor能够被所有元素继承:letter-spacing,word-spa ...
- sqlalchemy 的 ORM 方式使用示例
知乎: 使用 sqlalchemy 的 orm 方式操作数据库是一种怎样的体验? 答: 酸爽! 本文基于:win10 + python3.4 + sqlAlchemy 1.0.13 先看一个图(来源) ...
- 那些年我们写过的T-SQL(下篇)(转)
原文:http://www.cnblogs.com/wanliwang01/p/TSQL_Base04.html 下篇的内容很多都会在工作中用到,尤其是可编程对象,那些年我们写过的存储过程,有木有 ...
- 完美隐藏win7文件和文件夹
有没有一种方法即使使用隐藏模式也不能查看, 没错可以用上帝模式....... 啥是Win7上帝模式?不知道的看看..... <<<<<<<<<&l ...
- gunzip 和 unzip 解压文件到指定的目录
Linux 常用的压缩命令有 gzip 和 zip,两种压缩包的结尾不同:zip 压缩的后文件是 *.zip ,而 gzip 压缩后的文件 *.gz 相应的解压缩命令则是 gunzip 和 unzi ...
- Java系列:JVM指令详解(上)(zz)
一.未归类系列A 此系列暂未归类. 指令码 助记符 说明 59:iastore 60:lload 6 //因为str ...