【问题标题】:How to catch an unhandled exception thrown from a continuation?如何捕获从延续引发的未处理异常?
【发布时间】:2017-03-11 04:29:21
【问题描述】:

我无法捕获从延续任务引发的未处理异常。

为了演示这个问题,让我向您展示一些有效的代码。此代码来自一个基本的 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 中订阅的未处理异常事件之一捕获。

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

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


[编辑]

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 {} 捕获它。 msdn.microsoft.com/en-us/bb264489.aspx
  • @mybirthname 不,这是符合 CLS 的基本 InvalidOperationException。请注意,当未从后台任务中抛出异常时,该异常会被正确捕获。
  • @NiyokoYuliawan 这可能有效,但不幸的是(根据问题上的.net-4.0 标签)我无法使用它。
  • 是的,抱歉,我没有看到你的标签。
  • @NiyokoYuliawan 这是一个好主意,事实上,如果你可以使用它,它确实有效(可惜我不能!) - 我会在这个问题中添加一些内容!跨度>

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


【解决方案1】:

你检查过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");
}

【讨论】:

  • 不幸的是,这不起作用-我认为当我执行if (antecedent.Exception != null) 时会“观察到”异常,因此不会引发该事件(尽管即使我似乎也不会触发注释掉那一行...)
  • 你说得对.. 没有用 continuewith 检查 .. 找到东西后会更新我的答案
【解决方案2】:

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

您可以通过订阅TaskScheduler.UnobservedTaskException 来验证这一点在您继续完成后的一段时间后强制进行垃圾收集。

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

【讨论】:

  • 我明白你在说什么,但我不能手动调用异常处理程序,因为表单在类库中,异常处理程序在创建表单的应用程序中,所以我需要一种能够在“最后机会”外部异常处理程序中捕获异常的解决方案。看来我提出的解决方案可能是唯一可行的解​​决方案(无法使用async)。
  • 这个答案确实解释了为什么使用延续不起作用!
  • 嗯,这样做是肮脏的黑客和糟糕的设计,但是如果没有其他帮助,您可以使用 BeginInvoke。至少现在你知道为什么它不能继续工作了。
  • 那么,鉴于表单无法访问特定的异常处理程序并且您需要能够从应用程序报告任何未处理的异常,您将如何解决问题?如果发生不好的事情,抛出异常是正常的......(事实上,如果你可以使用await,这正是它的工作原理 - 它在调用代码的上下文中重新抛出)。
  • 我会为表单提供该异常处理程序 :) 无论创建该表单的什么都可以访问异常处理程序,因此可以以一种或另一种形式将其传递给表单构造函数(或设置某些表单属性)(一些 ILogger 接口)。如果您使用一个依赖注入容器,也可以使用它来完成。如果这一切都没有帮助 - 至少您可以将异常处理程序作为全局对象提供并使其可用于所有表单(将其放在单独的 dll 中)。
【解决方案3】:

更新

您提到您使用的是 .Net 4.0。使用async/await 功能怎么样,它不会阻塞用户界面。您只需从 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();
        }

【讨论】:

  • 这行得通,因为它把它变成了一个同步调用——唉,我们不能使用它,因为它阻塞了OnShown() 中的 UI——而不阻塞 UI 是使用背景的全部意义任务!
  • 你是对的。这不是一个答案,但我会把它留作个人历史。
【解决方案4】:

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

我决定走老路并使用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 的回答!

【讨论】:

    猜你喜欢
    • 2012-09-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-14
    • 2015-09-10
    • 2011-10-27
    • 1970-01-01
    • 2017-11-25
    相关资源
    最近更新 更多