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

时间:2016-10-28 09:45:25

标签: c# winforms exception .net-4.0 task

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

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

首先, 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中订阅的未处理的异常事件捕获。

不幸的是没有抓住任何东西,表格保持打开状态,没有迹象表明出现了任何问题。

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

[编辑]

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

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

4 个答案:

答案 0 :(得分:2)

您检查了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");
}

答案 1 :(得分:2)

即使你在UI线程上抛出异常,它意味着它最终会到达Application.ThreadException。在这种情况下,它被Task异常处理程序(设置Task.ExceptionTask.IsFaulted等的处理程序)拦截,实际上变为未观察到的任务异常。可以认为Task使用Control.Invok e来执行延续,Control.Invoke是同步的,这意味着它的异常无法传递给Application.ThreadException,因为winforms不知道如果它将由调用者处理(与Control.BeginInvoke不同,则异常将始终传递给Application.ThreadException)。

您可以通过在续订完成后订阅TaskScheduler.UnobservedTaskException 强制垃圾回收来验证这一点。

这么长的故事简短 - 在这种情况下,你的延续是否在UI线程上运行是无关紧要的 - 你的延续中所有未处理的异常将以UnobservedTaskException结束,而不是ThreadException。在这种情况下手动调用异常处理程序是合理的,而不是依赖那些“最后的”处理程序。

答案 2 :(得分:1)

<强>更新

您提到过您使用的是.Net 4.0。如何使用async/await功能,它不会阻止UI。您只需要从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();
        }

答案 3 :(得分: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的答案!