在 WPF 程序中,通常可以通过 Application.DispatcherUnhandledException
或 AppDomain.UnhandledException
事件来处理全局 未处理异常
,其中前者是由 WPF 框架提供的,后者是由 .NET Framework 提供的,后者能够捕获更多的未处理异常。对于 Task
中的未处理异常,这两种事件都不会触发,仅能通过 TaskScheduler.UnobservedTaskException
事件来捕获。另外,还有个 AppDomain.FirstChanceException
事件,每个异常都会引发该事件,即使该异常已被 try...catch
处理,此事件不在本文的讨论范围内。
e.Handled = true
来阻止程序崩溃所有线程(Task 除外)
抛出的未处理异常legacyUnhandledExceptionPolicy
配置异常策略 )Task
中抛出的未处理异常protected override void OnStartup(StartupEventArgs e)
{
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
Application.Current.DispatcherUnhandledException += Current_DispatcherUnhandledException;
base.OnStartup(e);
}
private void Current_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
MessageBox.Show($"Current_DispatcherUnhandledException:{e.Exception}");
e.Handled = true; // 标记为 “已处理”,避免异常进一步传递而引起崩溃
}
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
MessageBox.Show($"CurrentDomain_UnhandledException: {e.ExceptionObject}");
}
private void Button_Click(object sender, RoutedEventArgs e)
{
throw new InvalidOperationException();
}
对于一个 UI 线程抛出的未处理异常,其会先触发 DispatcherUnhandledException
事件,如果该事件处理方法中未标记 e.Handled
为 true
,则会进一步触发 UnhandledException
事件。
private void Button_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(() => { throw new InvalidOperationException(); });
thread.Start();
// ThreadPool.QueueUserWorkItem(state => { throw new InvalidOperationException(); });
}
此类未捕获异常仅会触发 UnhandledException 事件,并且事件参数中并未提供类似 e.Handled
的方法来阻止程序崩溃,通常仅在该事件处理方法中添加日志记录或用户提示。在 .NET 2.0
及以前的版本,此类未处理异常是不会引起程序崩溃的,我们也可以通过配置来开启旧的异常处理策略,在 App.Config 中添加如下配置:
<configuration>
<runtime>
<legacyUnhandledExceptionPolicy enabled="1"/>
runtime>
configuration>
private void Button_Click(object sender, RoutedEventArgs e)
{
var task = new Task(() =>
{
throw new InvalidOperationException();
});
task.Start();
// Task.Run(() => throw new InvalidOperationException());
// Task.Factory.StartNew(() => throw new InvalidOperationException());
// var worker = new BackgroundWorker();
// worker.DoWork += (s, ex) => { throw new InvalidOperationException(); };
// worker.RunWorkerAsync();
}
如上的代码不会触发 UnhandledException
事件,也不会引起程序奔溃。如果想从全局捕获此类未处理异常,可注册 TaskScheduler.UnobservedTaskException
事件。
protected override void OnStartup(StartupEventArgs e)
{
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
base.OnStartup(e);
}
private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
MessageBox.Show($"TaskScheduler_UnobservedTaskException: {e.Exception}");
e.SetObserved(); // 标识异常已被观察,不会传给系统,避免崩溃
}
此事件并非在抛出异常后立即触发,其依赖于垃圾回收,在某次垃圾收集过程,从 Finalizer 线程里触发并执行。可通过如下方式来强制垃圾回收,及时触发事件(实际工程中避免这些操作,会有性能问题)。
private void Button_Click(object sender, RoutedEventArgs e)
{
var task = Task.Run(() => throw new NotImplementedException());
((IAsyncResult)task).AsyncWaitHandle.WaitOne();
task = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
另外,还可将 Task 中的异常转到调度线程中,从而引发 UnhandledException 事件,Task.Result、Task.Wait() 等都可实现此效果。
private void Button_Click(object sender, RoutedEventArgs e)
{
var task = Task.Run(() => throw new NotImplementedException());
task.Wait();
}
在 托管代码
中调用 非托管
接口,部分未处理异常是无法接住的,会直接引起程序崩溃。如下所示,C++
中实现了 Add(int x, int y)
方法,在 C#
中调用之,前面的未处理异常事件均不会触发,程序会直接崩溃。
extern "C" _declspec(dllexport) int Add(int x, int y)
{
char *p = nullptr;
*p = '1'; // 此处会抛异常
return x + y;
}
[DllImport("MyDll")]
public static extern int Add(int x, int y);
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
Add(1, 2);
}
结合前面 Task
内部异常的特性,可以将调用代码放在 Task
中,以避免程序崩溃。Task
是个神奇的东西,还需要进一步深入学习。
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
Task.Run(() => Add(1, 2));
}