.NET Core学习笔记(3)——async/await中的Exception处理
在写了很多年.net程序之后,年长的猿类在面对异步编程时,仍不时会犯下致命错误,乃至被拖出去杀了祭天。本篇就async/await中的exception处理进行讨论,为种族的繁衍生息做出贡献……
处理async/await中的exception,最致命的莫过于想抓的exception抓不到,程序崩的莫名其妙,连日志都没记下来,没法定位错误。让我们来看以下代码:
private async void somethingwrongasync() { await task.delay(100); throw new invalidoperationexception(); } public void somethingwrongcannotcatch() { try { somethingwrongasync(); } catch (exception) { // sometimes we write log here, but the exception is never caught! throw; } }
somethingwrongasync是一个标准的async方法。在这个方法中,我们主动抛出了invalidoperationexception。我们在方法somethingwrongcannotcatch中调用了somethingwrongasync。但是非常遗憾,这里的try catch无法捕捉到invalidoperationexception。
包含以上代码的sample工程是一个wpf程序,代码链接:
https://github.com/manupstairs/asyncawaitpractice
在测试之前,我们可以在throw那一行打个断点,f5起来后,点击mainwindow的somethingwrongcannotcatch按钮。非常遗憾程序崩了,并且没有进入断点。
这意味着如果我们想在这个try catch里对exception做出处理,甚至仅仅记录日志,都是一个不可能完成的任务。如果我们在wpf工程的app.xaml.cs里添加如下代码:
public partial class app : application { public app() { this.dispatcherunhandledexception += (sender, e) => { debug.writeline(e); }; } }
确实是可以捕捉到这个异常,不过在dispatcherunhandledexception事件中,我们已经错过了处理exception的时机,能做的也仅仅是记录日志。这并不是正确的处理异常的方式。
让我们来看另一段稍有不同的代码:
private async task taskwrongasync() { await task.delay(100); throw new invalidoperationexception(); } public void taskwrongwithnothing() { try { taskwrongasync(); } catch (exception) { // sometimes we write log here, but the exception is never caught! throw; } }
除方法名外,代码仅做了些微的改变,throw new invalidoperationexception的taskwrongasync方法,把返回类型从void改为了task。按f5运行,点击mainwindow的按钮taskwrongwithnothing。似乎什么也没有发生,即使dispatcherunhandledexception事件也无法捕获任何异常。在真实的项目中,很可能taskwrongasync已经破坏了程序的状态,却没有被任何人察觉。
其实visual studio已经嗅出了代码的坏味道,每一个warning都可能是致命的。在这里我们按照智能提示修复这个warning,再重新调试看看。
public async void taskwrongbutcatch() { try { await taskwrongasync(); } catch (exception) { throw; } }
通过taskwrongbutcatch方法,我们可以在catch中成功捕获invalidoperationexception。接着在被我们throw后,也可以成功触发dispatcherunhandledexception事件。
接下来对这三种写法的区别做出一些解释,通常async task方法是将exception置于task对象中,在exception发生时,task的状态将变成faulted,然后在执行await操作时,由task将exception抛回给调用线程,所以我们可以通过try catch来捕获。
而第一种async void方法,因为返回值没有task,无法通过await操作将exception抛回调用线程。async void方法中的exception将在synchronizationcontext 上抛出,这种情况下无法在async void方法的外部捕捉到exception。
正确的做法是,避免写async void方法,而是通过task来返回。只有在作为event处理方法时,才应该编写async void的方法。
第二个例子中我们犯下了更为可怕的错误,exception被完全掩盖了。第一个例子中虽然我们不能在async void方法外部捕获exception,但实际exception对wpf程序而言是可见的,可以通过dispatcherunhandledexception观察到。而有了task却不await,程序不知道task何时结束。这个exception会一直到task被请求结果时,才会被抛出来。我们可以试试如下代码,异常会在请求result时被抛出。
static void main(string[] args) { new program().taskintwrongwithresult(); console.readkey(); } private async task<int> taskintwrongasync() { await task.delay(100); throw new invalidoperationexception(); } public void taskintwrongwithresult() { var result = taskintwrongasync().result; console.writeline(result); }
相对于dispatcherunhandledexception事件,我们确实也可以通过taskscheduler.unobservedtaskexception事件来检测task中未被抛出的exception。但在这里我们能做的仅仅是记录日志,实际绝对不推荐不给task应用await关键字。
综上所述,async/await异步方法的exception处理应遵循如下原则:
• 尽量避免async void,而采用async task方式。
• 应用await给每一个task返回值。
• 使用async void 作为异步方法链的终结点时,加上try…catch。
• 同理可以推测出对于async lamdba,不要使用action委托类型,而应该始终使用func<task>这样有task返回的委托类型。
• 通过taskscheduler.unobservedtaskexception事件来检测漏网之鱼。
本篇所有代码见github:
https://github.com/manupstairs/asyncawaitpractice