TAP全局异常处理程序

21

这段代码会抛出一个异常。有没有可能定义一个应用全局处理程序来捕获它?

string x = await DoSomethingAsync();

使用 .net 4.5 / WPF


4
在那个语句周围使用try/catch有什么问题吗? - Charles Prakash Dasari
6
触发创建 Applicaton.DispatcherUnhandledException 的相同原因。 - user3230660
这是为了处理那些您无法捕获的异常而创建的。一般情况下,您应该捕获可能在代码中出现的异常。然而,以下是一些链接,可能会帮助您解决问题:https://dev59.com/amzXa4cB1Zd3GeqPYNQl和http://msdn.microsoft.com/en-us/library/system.appdomain.unhandledexception%28v=vs.110%29.aspx - Charles Prakash Dasari
1
这段代码叫什么?你已经尝试过什么了? - Stephen Cleary
5个回答

22

如果我理解正确,这实际上是一个很好的问题。一开始我投票赞成关闭它,但现在撤回了我的投票。

重要的是要理解在 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”。

1
Noseratio,你完全理解了我的问题。然而,直到运行GC才引发UnobservedTaskException这一事实使其在功能上毫无用处。Toub在他的帖子的问题部分提供了IgnoreExceptions解决方案。我不明白为什么编译器在创建异步方法包装器时不添加它。在继续执行中,如果处理该事件,它应该引发UnobservedTaskExpception。 - user3230660
1
@Sam,我认为这种行为完全合理。Task对象是一个承诺,一个延迟的结果。它刚刚完成并不意味着必须立即被观察到。它可能在10分钟后通过与其他任务(例如WhenAll)的组合或其他异步逻辑的特定方式进行观察。为什么编译器要对此做出任何假设并破坏这样的逻辑呢?它不应该这样做。 - noseratio - open to work
你完全掌控这个过程。可以在可能发生异常的地方使用 try/catch 处理异常,或者使用前面提到的 IgnoreException 将其标记为已观察到。 - noseratio - open to work
1
谢谢Noseratio,我的意思是我需要一种创建“最后手段观察者”或全局处理程序的方法。如果我处理DispatcherUnhandledException,我不是在要求编译器做出假设 - 我正在给它一个具体的指令。如果编译器添加Toubs IgnoreExceptions或类似的东西,如果我处理它,它可以触发DispatcherUnhandledException。 - user3230660
1
顺便提一下,看看Todd Menier提供的答案。虽然他的答案是错误的,但他的推理是正确的。也就是说 - 我可以为非异步应用程序定义全局处理程序,为什么不能为异步应用程序定义一个呢? - user3230660
显示剩余8条评论

1
你可以始终使用 Application.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);

1
在.NET 4.5中,在async代码中,您可以通过注册TaskScheduler.UnobservedTaskException事件的处理程序来处理未观察到的异常。如果您没有访问Task.ResultTask.Exception属性并且没有调用Task.Wait,则认为异常是未观察到的。
在未观察到的异常到达TaskScheduler.UnobservedTaskException事件处理程序后,默认行为是吞噬该异常,以使程序不会崩溃。可以通过添加以下内容到配置文件来更改此行为:
<configuration> 
   <runtime> 
      <ThrowUnobservedTaskExceptions enabled="true"/> 
   </runtime> 
</configuration>

正确,但在这种情况下,他正在等待结果,因此任务将被视为已观察到。 - Todd Menier
这在 .NET 4.0 中是正确的,但在 .NET 4.5+ 中不再适用。有关详细信息,请查看我的答案 - noseratio - open to work

1

将事件绑定到AppDomain.CurrentDomain.FirstChanceException,可以确保捕获异常。正如@Noseratio所指出的那样,即使异常在catch块中得到优雅处理并且应用程序继续运行,您也会收到应用程序中每个异常的通知。

然而,我仍然认为这个事件至少对于捕获应用程序停止前抛出的最后几个异常或其他调试场景很有用。

如果你想保护自己免受此类情况的影响

string x = await DoSomethingAsync();

我给你的建议是,不要那样做,添加一个try catch块 :-)


0

那么,在这种情况下,你会如何定义一个应用程序全局处理程序来处理异常?

string x = DoSomething();

很有可能你问题的答案是完全相同的。看起来你已经正确地等待了一个异步方法,编译器会尽最大努力确保在异步方法中发生的任何异常都以一种传播和解开的方式处理,使你可以像在同步代码中一样处理它。这是异步/等待的主要优点之一。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接