这段代码会抛出一个异常。有没有可能定义一个应用全局处理程序来捕获它?
string x = await DoSomethingAsync();
使用 .net 4.5 / WPF
这段代码会抛出一个异常。有没有可能定义一个应用全局处理程序来捕获它?
string x = await DoSomethingAsync();
使用 .net 4.5 / WPF
如果我理解正确,这实际上是一个很好的问题。一开始我投票赞成关闭它,但现在撤回了我的投票。
重要的是要理解在 async Task
方法内部抛出异常如何传播到外部。最重要的是,处理任务完成的代码需要观察此类异常。
例如,这里有一个简单的 WPF 应用程序,我使用 NET 4.5.1:
using System;
using System.Threading.Tasks;
using System.Windows;
namespace WpfApplication_22369179
{
public partial class MainWindow : Window
{
Task _task;
public MainWindow()
{
InitializeComponent();
AppDomain.CurrentDomain.UnhandledException +=
CurrentDomain_UnhandledException;
TaskScheduler.UnobservedTaskException +=
TaskScheduler_UnobservedTaskException;
_task = DoAsync();
}
async Task DoAsync()
{
await Task.Delay(1000);
MessageBox.Show("Before throwing...");
GCAsync(); // fire-and-forget the GC
throw new ApplicationException("Surprise");
}
async void GCAsync()
{
await Task.Delay(1000);
MessageBox.Show("Before GC...");
// garbage-collect the task without observing its exception
_task = null;
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
}
void TaskScheduler_UnobservedTaskException(object sender,
UnobservedTaskExceptionEventArgs e)
{
MessageBox.Show("TaskScheduler_UnobservedTaskException:" +
e.Exception.Message);
}
void CurrentDomain_UnhandledException(object sender,
UnhandledExceptionEventArgs e)
{
MessageBox.Show("CurrentDomain_UnhandledException:" +
((Exception)e.ExceptionObject).Message);
}
}
}
一旦抛出ApplicationException
,它就不可观察。既不会调用TaskScheduler_UnobservedTaskException
也不会调用CurrentDomain_UnhandledException
。异常会保持静态,直到_task
对象被等待或等待。在上面的示例中,它从未被观察到,因此只有在任务被垃圾回收时才会调用TaskScheduler_UnobservedTaskException
。然后这个异常将被吞噬。
可以通过在app.config
中配置ThrowUnobservedTaskExceptions
来启用旧的.NET 4.0行为,其中AppDomain.CurrentDomain.UnhandledException
事件将被触发并且应用程序崩溃:
<configuration>
<runtime>
<ThrowUnobservedTaskExceptions enabled="true"/>
</runtime>
</configuration>
当以这种方式启用时,AppDomain.CurrentDomain.UnhandledException
仍将在垃圾回收异常后触发,而不是在抛出异常的地方立即触发。
Stephen Toub 在他的博客文章《.NET 4.5 中的任务异常处理》中描述了这种行为。关于任务垃圾回收的部分在帖子的评论中有介绍。
对于async Task
方法,情况就是这样。但对于通常用于事件处理程序的async void
方法,则完全不同。我们来这样更改代码:
public MainWindow()
{
InitializeComponent();
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
this.Loaded += MainWindow_Loaded;
}
async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
await Task.Delay(1000);
MessageBox.Show("Before throwing...");
throw new ApplicationException("Surprise");
}
因为它是“async void”,所以没有“Task”引用来保持(因此没有可能在以后进行观察或垃圾回收)。在这种情况下,异常会立即在当前同步上下文中抛出。对于 WPF 应用程序,将首先触发“Dispatcher.UnhandledException”,然后是“Application.Current.DispatcherUnhandledException”,最后是“AppDomain.CurrentDomain.UnhandledException”。最后,如果没有处理这些事件(“EventArgs.Handled”未设置为“true”),无论“ThrowUnobservedTaskExceptions”设置如何,应用程序都将崩溃。“TaskScheduler.UnobservedTaskException”在这种情况下不会被触发,原因是相同的:没有“Task”。Task
对象是一个承诺,一个延迟的结果。它刚刚完成并不意味着必须立即被观察到。它可能在10分钟后通过与其他任务(例如WhenAll
)的组合或其他异步逻辑的特定方式进行观察。为什么编译器要对此做出任何假设并破坏这样的逻辑呢?它不应该这样做。 - noseratio - open to worktry/catch
处理异常,或者使用前面提到的 IgnoreException
将其标记为已观察到。 - noseratio - open to workApplication.DispatcherUnhandledException
方法来处理异常。当然,它会在一个 TargetInvocationException
中给出,并且可能不像其他方法那样美观。但它完全可以正常工作。_executeTask = executeMethod(parameter);
_executeTask.ContinueWith(x =>
{
Dispatcher.CurrentDispatcher.Invoke(new Action<Task>((task) =>
{
if (task.Exception != null)
throw task.Exception.Flatten().InnerException;
}), x);
}, TaskContinuationOptions.OnlyOnFaulted);
async
代码中,您可以通过注册TaskScheduler.UnobservedTaskException
事件的处理程序来处理未观察到的异常。如果您没有访问Task.Result
,Task.Exception
属性并且没有调用Task.Wait
,则认为异常是未观察到的。TaskScheduler.UnobservedTaskException
事件处理程序后,默认行为是吞噬该异常,以使程序不会崩溃。可以通过添加以下内容到配置文件来更改此行为:<configuration>
<runtime>
<ThrowUnobservedTaskExceptions enabled="true"/>
</runtime>
</configuration>
将事件绑定到AppDomain.CurrentDomain.FirstChanceException
,可以确保捕获异常。正如@Noseratio所指出的那样,即使异常在catch块中得到优雅处理并且应用程序继续运行,您也会收到应用程序中每个异常的通知。
然而,我仍然认为这个事件至少对于捕获应用程序停止前抛出的最后几个异常或其他调试场景很有用。
如果你想保护自己免受此类情况的影响
string x = await DoSomethingAsync();
我给你的建议是,不要那样做,添加一个try catch块 :-)
那么,在这种情况下,你会如何定义一个应用程序全局处理程序来处理异常?
string x = DoSomething();
很有可能你问题的答案是完全相同的。看起来你已经正确地等待了一个异步方法,编译器会尽最大努力确保在异步方法中发生的任何异常都以一种传播和解开的方式处理,使你可以像在同步代码中一样处理它。这是异步/等待的主要优点之一。