上一篇文章里咱们讨论了某些async/await的用法中出现遗漏异常的状况,而且谈到该如何使用WhenAll辅助方法来避免这种状况。WhenAll辅助方法将会汇总一系列的任务对象,一旦其中某个出错,则会抛出“其中一个”异常。那么到底是哪一个异常?若是咱们要处理全部的异常怎么办?咱们此次就来详细讨论await操做在异常分派时的相关行为。html
await抛出异常时的行为编程
要理解await的行为,仍是从理解Task对象的异常表现开始。Task对象有一个Exception属性,类型为AggregateException,在执行成功的状况下该属性返回null,不然便包含了“全部”出错的对象。既然是AggregateException,则意为着可能包含多个子异常,这种状况每每会在任务的父子关系中出现,具体状况能够参考MSDN中的相关说明。在许多状况下一个Task内部只会出现一个异常,此时这个AggregateException的InnerExceptions属性天然也就只一个元素。数组
Task对象自己还有一个Wait方法,它会阻塞当前执行代码,直到任务完成。在出现异常的时候,它会将自身的AggregateException抛出:async
try { t.Wait(); } catch (AggregateException ex) { ... }
Wait方法是“真阻塞”,而await操做则是使用阻塞语义的代码实现非阻塞的效果,这个区别必定要分清。与Wait方法不一样的是,await操做符效果并不是是“抛出”Task对象上的Exception属性,而只是抛出这个AggregateException对象上的“其中一个”元素。我在内部邮件列表中询问这么作的设计考虑,C#开发组的同窗回答道,这个决策在内部也经历了激烈的争论,最终的选择这种方式而不是直接抛出Task对象上的AggregateException是为了不编写出冗余的代码,并让代码与传统同步编程习惯更为接近。spa
他们举了一个简单的示例,假如一个Task对象t可能抛出两种异常,如今的错误捕获方式为:设计
try { await t1; } catch (NotSupportedException ex) { ... } catch (NotImplementedException ex) { ... } catch (Exception ex) { ... }
假如await操做抛出的是AggregateException,那么代码就必须写为:code
try { await t1; } catch (AggregateException ex) { var innerEx = ex.InnerExceptions[0]; if (innerEx is NotSupportedException) { ... } else if (innerEx is NotImplementedException) { ... } else { ... } }
显然前者更贴近传统的同步编程习惯。可是问题在于,若是这个Task中包含了多个异常怎么办?以前的描述是抛出“其中一个”异常,对于开发者来讲,“其中一个”这种模糊的说法天然没法使人满意,但事实的确如此。从内部邮件列表中的讨论来看,C#开发团队提到他们“故意”不提供文档说明究竟会抛出哪一个异常,由于他们并不想作出这方面的约束,由于这部分行为一旦写入文档,便成为一个规定和限制,为了类库的兼容性从此也没法对此作出修改。htm
他们也提到,若是单论目前的实现,await操做会从Task.Exception.InnerExceptions集合中挑出第一个异常,并对外“抛出”,这是System.Runtime.CompilerServices.TaskAwaiter类中定义的行为。可是既然这并不是是“文档化”的固定行为,开发人员也尽可能不要依赖这点。对象
WhenAll的异常汇总方式blog
其实这个话题跟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(3000, new Exception("Ex3")), ThrowAfter(1000, new Exception("Ex1"))), ThrowAfter(2000, new Exception("Ex2")))); } catch (Exception ex) { ... }
这段代码使用了嵌套的WhenAll方法,总共会出现三个异常,按其抛出的时机排序,其顺序为Ex1,Ex2及Ex3。那么请问:
结果以下:
这里咱们也顺即可以得知,若是您不想捕获AggregateException集合中的“其中一个”异常,而是想处理全部异常的话,也能够写这样的代码:
Task all = null; try { await (all = Task.WhenAll( ThrowAfter(1000, new Exception("Ex1")), ThrowAfter(2000, 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(2000, new NotSupportedException("Ex1")), ThrowAfter(1000, 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 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 30 --- End of inner exception stack trace --- ---> (Inner Exception #0) System.NotSupportedException: Ex1 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 30<---
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(2000, new NotSupportedException("Ex1")), ThrowAfter(1000, new NotImplementedException("Ex2")))); } catch { throw all.Exception; }
此时打印的结果即是一个AggregateException包含着另外一个AggregateException,其中包含了Ex1和Ex2。为了“解开”这种嵌套关系,AggregateException也提供了一个Flatten方法,能够将这种嵌套彻底“铺平”,例如:
SomeTask().ContinueWith(t => PrintException(t.Exception.Flatten()));
此时打印的结果便直接是一个AggregateException包含着Ex1与Ex2了。
相关文章
关于C#中async/await中的错误处理(上)
关于C#中async/await中的错误处理(下)