一、背景
某天,應用程式程式無緣無故退出,也就是我們通常說的崩潰。通常情況下,windows事件會記錄一條訊息。但是有時候,我們發現這樣的資訊,對於查詢問題,還是遠遠不夠的,因為它說RunTime報錯。這時,我就想能不能自己捕獲全域性未處理的異常。之所以有這樣的想法,因為之前在客戶端程式中寫過。這次我要在.netcore中處理,網上搜了一段程式碼,高高興興地貼上去了,覺得上了保險箱。
二、探索
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; private static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { foreach (var item in e.Exception.InnerExceptions) { Logger.Error("未捕獲的Task異常 " + item.InnerException.Message + " " + item.GetType().Name); } }
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { Exception exception = (Exception)e.ExceptionObject; Logger.Error("未捕獲的Domain異常 : " + exception.Message + "," + exception.StackTrace); Logger.Error("Runtime terminating: {0}", e.IsTerminating); }
給AppDomain和TaskScheduler註冊了兩個未處理異常的方法,等系統丟擲異常時,可以捕獲。沒想到,程式一天之內崩潰了兩次,比之前幾個月崩潰一次,頻率不知高了都少倍。下面是崩潰時的資訊:
這堆疊資訊,得仔細看,才能看出門道,否則,可能會把重要的資訊遺漏掉。猛的一看,程式哪裡有未將物件引用到例項了?在業務程式碼中苦苦思索,沒有找到。第二天早晨,仔細檢視這個錯誤資訊,發現這個異常竟然是TaskScheduler註冊的這個方法裡面報出來的。於是我再次修改程式碼:
private static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { if (e.Exception?.InnerException != null) { foreach (var item in e.Exception.InnerExceptions) { Logger.Error("未捕獲的Task異常 " + item.InnerException.Message + " " + item.GetType().Name); } } else { Logger.Error("[Exception]未捕獲的Task異常 " + e.Exception?.Message + " " + e.Exception?.StackTrace); } //將異常標識為已經觀察到 e.SetObserved(); }
程式碼是修改好了,在本地如何除錯呢?網上說,GC回收Task的時候,會觸發Task裡的異常,這個說法,應該是正確的,請看上面的堆疊資訊,回收的時候,會報異常釋出出去。好,那我就人為製造一個異常:
Task.Run(() => { throw new Exception("測試異常"); }); Thread.Sleep(2000); GC.Collect();
可是程式碼跑起來,沒有捕獲到任何異常。我以為GC沒有執行,我在網上搜尋答案,類似這樣的寫法:
Task.Run(() =>
{
throw new Exception("測試異常");
});
while(true){ //不停地給陣列分配記憶體 //呼叫GC }
這次程式碼執行起來,不僅異常沒有捕獲到,程式直接崩潰,說記憶體不足,最後筆記本發燙,導致了藍色畫面。我不得不重啟電腦。
三、處理
網上一篇文章說,在Debug模式下,捕獲不到異常。Release下可以。於是,我切換了模式,果然可以。
Logger.Error("未捕獲的Task異常 " + item.InnerException.Message + " " + item.GetType().Name);
在處理全域性異常的方法裡,我記錄了日誌,就這一句引發了未將物件引用到例項,除錯發現 itm.InnerException為null,所以呼叫Message就異常了。下面,我們來處理:
private static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) {
foreach (var item in e.Exception.InnerExceptions){
Logger.Error("未捕獲的Task異常 " + item.InnerException?.Message + " " + item.GetType().Name); }
}
處理好了,日誌輸出:未捕獲的Task異常 Exception,從除錯角度看,這樣的資訊,就是個廢話,改改程式碼:
private static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { foreach (var item in e.Exception.InnerExceptions) { Logger.Error("未捕獲的Task異常 " + item.Message + "," + item.StackTrace); } }
除錯結果如下:
這樣程式碼就好了嗎?我擔心儘管處理好後,程式還會退出,網上搜了下,可以加入這句:
//將異常標識為已經觀察到 e.SetObserved();
經過除錯,發現少了這句,也不會有問題,這句意思是不讓異常繼續往上冒泡,到此為止。這樣,程式就好了嗎?還是有所擔心,終極版的程式碼:
private static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { try { foreach (var item in e.Exception.InnerExceptions) { Logger.Error("未捕獲的Task異常 " + item.Message + "," + item.StackTrace); } } catch (Exception ex) { Logger.Error($"TaskScheduler_UnobservedTaskException處理異常:{ex.Message}"); } //阻止異常冒泡 e.SetObserved(); }
這裡之所以加上try..catch,因為擔心Logger出現異常,程式照樣會崩潰。所以,既想捕獲應用程式中Task中的異常,又不想因此把程式整垮。
四、後記
網上的程式碼,僅供參考和學習,要上伺服器,還得經過本地嚴格測試,誰知道會什麼時候會引發災難。