如何捕获从延续抛出的未处理异常?

How to catch an unhandled exception thrown from a continuation?

我无法捕获从延续任务中抛出的未处理异常。

为了演示这个问题,让我向您展示一些有效的代码。此代码来自基本 Windows Forms 应用程序。

首先,program.cs:

using System;
using System.Windows.Forms;

namespace WindowsFormsApplication3
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);

            Application.ThreadException += (sender, args) =>
            {
                MessageBox.Show(args.Exception.Message, "ThreadException");
            };

            AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
            {
                MessageBox.Show(args.ExceptionObject.ToString(), "UnhandledException");
            };

            try
            {
                Application.Run(new Form1());
            }

            catch (Exception exception)
            {
                MessageBox.Show(exception.Message, "Application.Run() exception");
            }
        }
    }
}

这会订阅所有可用的异常处理程序。 (实际上只提出了 Application.ThreadException,但我想确保排除所有其他可能性。)

下面是导致未处理异常正确显示消息的表单:

using System;
using System.Threading;
using System.Windows.Forms;

namespace WindowsFormsApplication3
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        protected override void OnShown(EventArgs e)
        {
            base.OnShown(e);
            doWork();
            this.Close();
        }

        void doWork()
        {
            Thread.Sleep(1000); // Simulate work.
            throw new InvalidOperationException("TEST");
        }
    }
}

当您运行执行此操作时,一秒钟后会出现一个消息框,显示异常消息。

如您所见,我正在编写一个 "please wait" 样式的表单,它会执行一些后台工作,然后在工作完成后自动关闭。

据此,我在OnShown()中添加了一个后台任务如下:

protected override void OnShown(EventArgs e)
{
    base.OnShown(e);

    Task.Factory.StartNew(doWork).ContinueWith
    (
        antecedent =>
        {
            if (antecedent.Exception != null)
                throw antecedent.Exception;

            this.Close();
        },
        TaskScheduler.FromCurrentSynchronizationContext()
    );
}

我认为延续将在表单的上下文中 运行,异常会以某种方式被我在 program.cs 中订阅的未处理异常事件之一捕获。

不幸的是,没有发现任何问题,表格保持打开状态,没有任何迹象表明有任何问题。

有谁知道我应该怎么做,这样如果在工作任务中抛出异常并且没有明确捕获和处理,它将被外部未处理的异常事件捕获?


[编辑]

Niyoko Yuliawan 建议使用 await。不幸的是我不能使用那个,因为这个项目被困在使用古老的.Net 4.0。但是,我可以确认使用 await 可以解决问题![​​=21=]

为了完整起见,如果我使用 .Net 4.5 或更高版本,这里是我可以使用的更简单和更易读的解决方案:

protected override async void OnShown(EventArgs e)
{
    base.OnShown(e);
    await Task.Factory.StartNew(doWork);
    this.Close();
}

[EDIT2]

raidensan 还提出了一个看起来很有用的答案,但不幸的是,这也不起作用。我认为这只是稍微解决了问题。以下代码也无法导致显示异常消息 - 即使 运行 在调试器下使用它并在行 antecedent => { throw antecedent.Exception; } 上设置断点表明正在到达该行。

protected override void OnShown(EventArgs e)
{
    base.OnShown(e);

    var task = Task.Factory.StartNew(doWork);

    task.ContinueWith
    (
        antecedent => { this.Close(); },
        CancellationToken.None,
        TaskContinuationOptions.OnlyOnRanToCompletion, 
        TaskScheduler.FromCurrentSynchronizationContext()
    );

    task.ContinueWith
    (
        antecedent => { throw antecedent.Exception; },
        CancellationToken.None,
        TaskContinuationOptions.OnlyOnFaulted,
        TaskScheduler.FromCurrentSynchronizationContext()
    );
}

你检查过TaskScheduler.UnobservedTaskException事件here了吗? 有了这个,您可以在垃圾收集发生后捕获工作线程中发生的未观察到的异常。

    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);

        TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;

        Application.ThreadException += (sender, args) =>
        {
            MessageBox.Show(args.Exception.Message, "ThreadException");
        };

        AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
        {
            MessageBox.Show(args.ExceptionObject.ToString(), "UnhandledException");
        };

        try
        {
            Application.Run(new Form1());
        }

        catch (Exception exception)
        {
            MessageBox.Show(exception.Message, "Application.Run() exception");
        }
    }
    private static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
    {
        throw e.Exception;
    }

这是任务:

protected override void OnShown(EventArgs e)
{
    base.OnShown(e);
    Task.Factory.StartNew(doWork);
    Thread.Sleep(2000);
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

void doWork()
{
    Thread.Sleep(1000); // Simulate work.
    throw new InvalidOperationException("TEST");
}

更新

您提到您使用的是.Net 4.0。使用 async/await 功能怎么样,它不会阻止 UI。您只需要从 nuget 添加 Microsoft Async 到您的项目。

现在修改OnShown如下(我添加了doWork的代码,这样可以更明显):

        protected async override void OnShown(EventArgs e)
        {
            base.OnShown(e);

            await Task.Factory.StartNew(() => 
            {
                Thread.Sleep(1000); // Simulate work.
                throw new InvalidOperationException("TEST");
            })
                .ContinueWith
                (
                    antecedent => { this.Close(); },
                    CancellationToken.None,
                    TaskContinuationOptions.OnlyOnRanToCompletion,
                    TaskScheduler.FromCurrentSynchronizationContext()
                )
                .ContinueWith
                (
                    antecedent => { 
                        throw antecedent.Exception;
                    },
                    CancellationToken.None,
                    TaskContinuationOptions.OnlyOnFaulted,
                    TaskScheduler.FromCurrentSynchronizationContext()
                );

        }

旧答案 找到解决方案,添加task.Wait();。不确定它为什么有效:

        protected override void OnShown(EventArgs e)
        {
            base.OnShown(e);

            var task = Task.Factory.StartNew(doWork)
                .ContinueWith
                (
                    antecedent => { this.Close(); },
                    CancellationToken.None,
                    TaskContinuationOptions.OnlyOnRanToCompletion,
                    TaskScheduler.FromCurrentSynchronizationContext()
                )
                .ContinueWith
                (
                    antecedent => { 
                        throw antecedent.Exception;
                    },
                    CancellationToken.None,
                    TaskContinuationOptions.OnlyOnFaulted,
                    TaskScheduler.FromCurrentSynchronizationContext()
                );

            // this is where magic happens.
            task.Wait();
        }

我找到了一个解决方法,我将 post 作为答案(但我不会将其标记为至少一天的答案,以防有人在此期间提出更好的答案!)

我决定走老路,使用 BeginInvoke() 而不是延续。然后它似乎按预期工作:

protected override void OnShown(EventArgs e)
{
    base.OnShown(e);

    Task.Factory.StartNew(() =>
    {
        try
        {
            doWork();
        }

        catch (Exception exception)
        {
            this.BeginInvoke(new Action(() => { throw new InvalidOperationException("Exception in doWork()", exception); }));
        }

        finally
        {
            this.BeginInvoke(new Action(this.Close));
        }
    });
}

但是,至少我现在知道为什么我的原始代码不起作用了。原因见evk的回答!

即使您在 UI 线程上抛出异常,它 而不是 意味着它最终会到达 Application.ThreadException。在这种情况下,它被Task exception handler(设置了Task.ExceptionTask.IsFaulted等的handler)拦截了,实际上变成了unobserved task exception。把它想象成 Task 使用 Control.Invoke 来执行你的延续,而 Control.Invoke 是同步的,这意味着它的异常不能传递给 Application.ThreadException,因为 winforms 不知道它是否将由调用者处理(这与 Control.BeginInvoke 不同,后者的异常将始终传递给 Application.ThreadException)。

您可以通过订阅 TaskScheduler.UnobservedTaskException 在您的继续完成后的某个时间强制垃圾回收来验证。

长话短说 - 在这种情况下,您的延续是否在 UI 线程上运行是无关紧要的 - 您的延续中所有未处理的异常将以 UnobservedTaskException 而不是 ThreadException 结束。在这种情况下手动调用异常处理程序而不依赖那些 "last resort" 处理程序是合理的。