在寫了很多年.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
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/71413.html
標籤:.NET Core
上一篇:Asp.net core 3.1+EF Core2.2.6+Oracle.EntityFrameworkCore2.1.19連接Oracle資料庫
