如果我理解正确的话,这实际上是一个好的问题。我最初投票关闭它,但现在撤回了我的投票。
了解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 仍将在TaskScheduler.UnobservedTaskException 异常被垃圾收集时触发,而不是在它抛出的地方。
Stephen Toub 在他的"Task Exception Handling in .NET 4.5" 博客文章中描述了这种行为。关于任务垃圾收集的部分在帖子的 cmets 中有描述。
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。