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

4
我无法捕获从续体任务抛出的未处理异常。
为了说明这个问题,让我展示一些可以工作的代码。这段代码来自一个基本的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");
        }
    }
}

当你运行这个程序时,一秒后会出现一个消息框,显示异常信息。
正如你所看到的,我正在编写一个“请稍候”样式的窗体,它会在后台完成一些工作,然后自动关闭。
因此,我在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就会解决这个问题!

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

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

[编辑2]

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()
    );
}

我不知道抛出的异常是否符合非CLS规范,但如果是,你应该只使用catch { }来捕获它。https://msdn.microsoft.com/zh-cn/bb264489.aspx - mybirthname
@mybirthname 不是的,这是一个基本的InvalidOperationException,它符合CLS。请注意,当不是从后台任务抛出时,异常会被正确捕获。 - Matthew Watson
@NiyokoYuliawan,这是一个好主意,实际上如果你能使用它,它确实有效(可惜我不能!)- 我会在问题中添加一些关于这个的内容! - Matthew Watson
我认为我找到了答案,请查看这里 - raidensan
@mybirthname 但是我应该把这样的等待放在哪里呢?我不能阻塞用户界面,所以我不能在 OnShown() 中等待,因此我使用了一个 continuation 来等待 doWork() 完成(这相当于等待任务完成)。 - Matthew Watson
显示剩余5条评论
4个回答

2
即使您在UI线程上抛出异常,也不意味着它最终会到达Application.ThreadException。在这种情况下,它被Task异常处理程序(设置Task.Exception、Task.IsFaulted等的处理程序)拦截,并实际上成为未观察到的任务异常。将其视为Task使用Control.Invoke执行您的继续操作,而Control.Invoke是同步的,这意味着来自它的异常无法传递给Application.ThreadException,因为winforms不知道它是否将由调用者处理(与Control.BeginInvoke不同,后者的异常将始终传递给Application.ThreadException)。
您可以通过订阅TaskScheduler.UnobservedTaskException并在完成继续操作后一段时间强制进行垃圾回收来验证这一点。
所以长话短说-在这种情况下,您的继续操作是否在UI线程上运行都是无关紧要的-所有未处理的异常都将作为UnobservedTaskException而结束,而不是ThreadException。在这种情况下,手动调用异常处理程序是合理的,而不是依赖于那些“最后的手段”处理程序。

我明白你的意思,但是我无法手动调用异常处理程序,因为表单在类库中,而异常处理程序在创建表单的应用程序中,所以我需要一个解决方案,可以在“最后机会”的外部异常处理程序中捕获异常。看起来我想出的解决方案可能是唯一可行的(无法使用async)。 - Matthew Watson
这个答案确实解释了为什么使用 continuation 不起作用! - Matthew Watson
那么,如果表单没有访问特定异常处理程序的权限,并且您需要能够报告应用程序中未处理的任何异常,您将如何解决该问题?如果发生错误,抛出异常是很正常的...(实际上,如果您可以使用await,那么它就是这样工作的-在调用代码的上下文中重新抛出)。 - Matthew Watson
嗯,我真的看不出有什么意义。任何代码都可能抛出异常,因此如果要正确报告错误,您总是需要一个外部异常处理程序-我认为没有什么特殊之处需要对其进行特殊的异常处理。您的方法最终将传递到系统中的每个表单中的异常处理程序,这听起来对我来说并不是一个好主意。请记住,这里的困难是表单的内部实现的产物,并且将其暴露给所有人并不是一个好计划,我想。 - Matthew Watson
但是使用当前的方法,您有点滥用了未处理的异常处理程序。首先,在这种情况下,异常并不是意外的 - 您预期它会发生,并且知道为什么以及在哪里发生。当您只重新抛出异常时,这些有价值的信息就会丢失。在那个“catch all”处理程序中,您拥有的只是调用堆栈(其中一部分在重新抛出时也会丢失)。如果您显式调用异常处理程序,则可以提供更多详细信息,这将在以后调试问题时帮助您。您可能不需要将处理程序传递给每个表单 - 您也可以使用一些全局(静态)类来执行此操作。 - Evk
显示剩余3条评论

2

你是否已经查看了 这里TaskScheduler.UnobservedTaskException 事件?使用它可以捕获在垃圾回收后发生在工作线程中的未观察到的异常。

    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");
}

不幸的是,那样做行不通 - 我认为当我执行 if (antecedent.Exception != null) 时异常被“观察”了,所以事件不会被抛出(即使我注释掉那一行似乎也没有触发...) - Matthew Watson
你说得对,我没有使用continuewith进行检查。等我找到一些资料后,我会更新我的答案。 - MKMohanty

1

更新

您提到了使用 .Net 4.0。考虑使用 async/await 特性,这不会阻塞用户界面。您只需要从 NuGet 中添加 Microsoft Async 到您的项目中即可。

enter image description here

现在将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();
        }

2
这样做是有效的,因为它将其转换为同步调用 - 不幸的是,我们不能在 OnShown() 中使用它,因为它会阻塞 UI - 而不阻塞 UI 是使用后台任务的整个重点! - Matthew Watson
你说得对。这不是一个答案,但我会把它留下来作为个人历史记录。 - raidensan

1
我找到了一个解决方法,将其发布为答案(但在此期间如果有更好的答案出现,我不会将其标记为答案!)
我决定采用老式的 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的答案,了解原因!


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