在同步编程中,一旦出现错误就会抛出异常,咱们可使用try…catch来捕捉异常,而未被捕获的异常则会不断向上传递,造成一个简单而统一的错误处理机制。不过对于异步编程来讲,异常处理一直是件麻烦的事情,这也是C#中async/await或是Jscex等异步编程模型的优点之一。可是,同步的错误处理机制,并不能彻底避免异步形式的错误处理方式,这须要必定实践规范来保证,至少咱们须要了解async/await究竟是如何捕获和分发异常的。在开发Jscex的过程当中,我也在C#内部邮件邮件列表中了解了不少关于TPL和C#异步特性的问题,错误处理也是其中之一。在此记录一下吧。html
使用try…catch捕获异常git
首先咱们来看下这段代码:github
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(1000, new NotSupportedException("Error 1")); var t2 = ThrowAfter(2000, 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到哪里去了?数组
未捕获的异常app
C#的async/await功能基于TPL的Task对象,每一个await操做符都是“等待”一个Task完成。在以前(或者说现在)的TPL中,Task对象的析构函数会查看它的Exception对象有没有被“访问”过,若是没有,且Task对象出现了异常,则会抛出这个异常,最终致使的结果每每即是进程退出。所以,咱们必须当心翼翼地处理每个Task对象的错误,不得遗漏。在.NET 4.5中这个行为被改变了,对于任何没有被检查过的异常,便会触发TaskSchedular.UnobservedTaskException事件——若是您不监听这个事件,未捕获的异常也就这么无影无踪了。异步
为此,咱们对Main方法进行一个简单的改造。async
static void Main(string[] args) { TaskScheduler.UnobservedTaskException += (_, ev) => PrintException(ev.Exception); _watch.Start(); MissHandling(); while (true) { Thread.Sleep(1000); GC.Collect(); } }
改造有两点,一是响应TaskScheduler.UnobservedTaskException,这天然没必要多说。还有一点即是不断地触发垃圾回收,以便Finalizer线程调用析构函数。现在这段代码除了打印出以前的信息以外,还会输出如下内容:ide
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(1000, new NotSupportedException("Error 1")); var t2 = ThrowAfter(2000, 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对象完成后又可能会有两种表现:
所有成功的状况自没必要说,那么在失败的状况下,什么叫作抛出“其中一个”异常?若是咱们要处理全部抛出的异常该怎么办?下次咱们继续讨论这方面的问题。
相关文章
关于C#中async/await中的异常处理(上)
关于C#中async/await中的异常处理(下)