異常處理之ThreadException、unhandledException及多線程異常處理
一:ThreadException和unhandledException的區別
處理未捕獲的異常是每個應用程序起碼有的功能,C#在AppDomain提供了UnhandledException 事件來接收未捕獲到的異常的通知。常見的應用如下:

{
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
}
static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Exception error = (Exception)e.ExceptionObject;
Console.WriteLine("MyHandler caught : " + error.Message);
}
未捕獲的異常,通常就是運行時期的BUG,于是我們可以在UnhandledException 的注冊事件方法CurrentDomain_UnhandledException中將未捕獲異常的信息記錄在日志中。值得注意的是,UnhandledException提供的機制并不能阻止應用程序終止,也就是說,CurrentDomain_UnhandledException方法執行后,應用程序就會被終止。
上面我們舉的例子來自于控制臺程序,UnhandledException可以在任何應用程序域中使用,在某些應用程序模型,如windows窗體程序,還存在ThreadException來處理 Windows 窗體線程中所發生的其未經處理的異常。即,在windows窗體程序中,使用 ThreadException 事件來處理 UI 線程異常,使用 UnhandledException 事件來處理非 UI 線程異常。ThreadException可以阻止應用程序終止。具體使用方法如下:

static void Main()
{
Application.ThreadException += new ThreadExceptionEventHandler(UIThreadException);
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
AppDomain.CurrentDomain.UnhandledException +=
new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
Application.Run(new ErrorHandlerForm());
}
private static void UIThreadException(object sender, ThreadExceptionEventArgs t)
{
try
{
string errorMsg = "Windows窗體線程異常 : \n\n";
MessageBox.Show(errorMsg + t.Exception.Message + Environment.NewLine + t.Exception.StackTrace);
}
catch
{
MessageBox.Show("不可恢復的Windows窗體異常,應用程序將退出!");
}
}
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
try
{
Exception ex = (Exception)e.ExceptionObject;
string errorMsg = "非窗體線程異常 : \n\n";
MessageBox.Show(errorMsg + ex.Message + Environment.NewLine + ex.StackTrace);
}
catch
{
MessageBox.Show("不可恢復的非Windows窗體線程異常,應用程序將退出!");
}
}
除了Windows窗體程序,再來說一下WPF程序。WPF的UI線程和Windows的UI線程有點不一樣。WPF的UI線程是交給一個叫做調度器的類:Dispatcher。代碼如下:

{
this.DispatcherUnhandledException += new DispatcherUnhandledExceptionEventHandler(Application_DispatcherUnhandledException);
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
}
void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
try
{
Exception ex = e.ExceptionObject as Exception;
string errorMsg = "非WPF窗體線程異常 : \n\n";
MessageBox.Show(errorMsg + ex.Message + Environment.NewLine + ex.StackTrace);
}
catch
{
MessageBox.Show("不可恢復的WPF窗體線程異常,應用程序將退出!");
}
}
private void Application_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
try
{
Exception ex = e.Exception;
string errorMsg = "WPF窗體線程異常 : \n\n";
MessageBox.Show(errorMsg + ex.Message + Environment.NewLine + ex.StackTrace);
}
catch
{
MessageBox.Show("不可恢復的WPF窗體線程異常,應用程序將退出!");
}
}
無論是Windows窗體程序還是WPF程序,我們都看到捕獲的異常當中分為"窗體線程異常"和"非窗體線程異常"。如在Windows窗體程序中,如果在窗體線程中,
將會觸發ThreadException事件。
{
throw new Exception("非窗體線程異常");
});
t.Start();
將會觸發UnhandledException事件,然后整個應用程序會被終止。
二:多線程異常處理
多線程的異常處理,要采用特殊的做法。以下的處理方式會存在問題:

{
Thread t = new Thread((ThreadStart)delegate
{
throw new Exception("多線程異常");
});
t.Start();
}
catch (Exception error)
{
MessageBox.Show(error.Message + Environment.NewLine + error.StackTrace);
}
應用程序并不會在這里捕獲線程t中的異常,而是會直接退出。從.NET2.0開始,任何線程上未處理的異常,都會導致應用程序的退出(先會觸發AppDomain的UnhandledException)。上面代碼中的try-catch實際上捕獲的還是當前線程的異常,而t是屬于新起的異常,所以,正確的做法應該是:

{
try
{
throw new Exception("多線程異常");
}
catch (Exception error)
{
MessageBox.Show("工作線程異常:" + error.Message + Environment.NewLine + error.StackTrace);
}
});
t.Start();
也就是說,新起的線程中異常的捕獲,可以將線程內部代碼全部try起來。原則上來說,每個線程自己的異常應該在自己的內部處理完畢,不過仍舊有一個辦法,可以將線程內部的異常傳遞到主線程。在Windows窗體程序中,可以使用窗體的BeginInvoke方法來將異常傳遞給主窗體線程:

{
try
{
throw new Exception("非窗體線程異常");
}
catch (Exception ex)
{
this.BeginInvoke((EventHandler)delegate
{
throw ex;
});
}
});
t.Start();
上文的代碼將最終引發主線程的Application.ThreadException。最終的結果看起來有點像:
在WPF窗體程序中,你可以采用如下的方法將工作線程的異常傳遞到主線程:

{
try
{
throw new Exception("非窗體線程異常");
}
catch (Exception ex)
{
this.Dispatcher.Invoke((EventHandler)delegate
{
throw ex;
}, System.Windows.Threading.DispatcherPriority.Normal, "", null);
}
});
t.Start();
WPF窗體程序的處理方式與Windows窗體程序比較,有三個很有意思的地方:第一個是,在Windows窗體中,我們采用的是BeginInvoke方法。你會發現使用Invoke方法,并不能引發主線程的Application.ThreadException。而在WPF窗體程序中,無論是調度器的Invoke還是BeginInvoke方法都能將異常傳遞給主線程。
第二個是,WPF窗體程序中,使用調度器的Invoke或BeginInvoke方式時,必須得加上最后兩個沒有用的參數,即""和null。否則引發的將是Invoke或者BeginInvoke方法的"參數計數不匹配"異常,而不是原始異常。關于這一點,可以在WPF主線程的Application的DispatcherUnhandledException事件處理方法中得到驗證,工作線程的異常將會包裝成為主線程異常的InnerException。
第三個地方就是InnerException。WPF的工作線程異常將會拋到主線程,變成主線程異常的InnerException,而Windows窗體程序的工作線程異常,將會被吃掉,直接變為null,只是在異常的Message信息中保存工作線程異常的Message。
三:ASP.NET異常處理
我們都知道ASP.NET的全局異常處理方法是Global中的Application_Error方法。我曾經查過ASP.NET的Appdomain.CurrentDomain.unhandledException,結果用反射得到的結果,unhandledException所注冊的事件方法根本不是這個方法。聯想到ASP.NET頁面,包括這個全局處理類,都是交給aspnet_isapi.dll處理的,而aspnet_isapi.dll不是一個托管程序集。所以,應該理解為,ASP.NET的未捕獲異常的處理,不同于托管異常(即CLR異常),而是交給aspnet_isapi.dll這個非托管DLL處理的。