在写了不少年.NET程序以后,年长的猿类在面对异步编程时,仍不时会犯下致命错误,乃至被拖出去杀了祭天。本篇就async/await中的Exception处理进行讨论,为种族的繁衍生息作出贡献……
处理async/await中的Exception,最致命的莫过于想抓的Exception抓不到,程序崩的莫名其妙,连日志都没记下来,无法定位错误。让咱们来看如下代码:git
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按钮。很是遗憾程序崩了,而且没有进入断点。github
这意味着若是咱们想在这个try catch里对Exception作出处理,甚至仅仅记录日志,都是一个不可能完成的任务。若是咱们在WPF工程的App.xaml.cs里添加以下代码:编程
public partial class App : Application { public App() { this.DispatcherUnhandledException += (sender, e) => { Debug.WriteLine(e); }; } }
确实是能够捕捉到这个异常,不过在DispatcherUnhandledException事件中,咱们已经错过了处理Exception的时机,能作的也仅仅是记录日志。这并非正确的处理异常的方式。异步
让咱们来看另外一段稍有不一样的代码:async
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事件。this
接下来对这三种写法的区别作出一些解释,一般async Task方法是将Exception置于Task对象中,在Exception发生时,Task的状态将变成Faulted,而后在执行await操做时,由Task将Exception抛回给调用线程,因此咱们能够经过try catch来捕获。spa
而第一种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