【问题标题】:Prevent outer exception from being discarded when thrown from BeginInvoke防止从 BeginInvoke 抛出时丢弃外部异常
【发布时间】:2011-12-20 04:41:28
【问题描述】:

我有一个Application.ThreadException 的处理程序,但我发现异常并不总是正确地传递给它。具体来说,如果我从 BeginInvoke 回调中抛出带有内部异常的异常,我的 ThreadException 处理程序不会得到 outer 异常——它只会得到 inner 异常。

示例代码:

public Form1()
{
    InitializeComponent();
    Application.ThreadException += (sender, e) =>
        MessageBox.Show(e.Exception.ToString());
}
private void button1_Click(object sender, EventArgs e)
{
    var inner = new Exception("Inner");
    var outer = new Exception("Outer", inner);
    //throw outer;
    BeginInvoke(new Action(() => { throw outer; }));
}

如果我取消注释 throw outer; 行并单击按钮,则消息框会显示外部异常(及其内部异常):

System.Exception:外部 ---> System.Exception:内部
--- 内部异常堆栈跟踪结束 ---
在 C:\svn\trunk\Code Base\Source.NET\WindowsFormsApplication1\Form1.cs:line 55 中的 WindowsFormsApplication1.Form1.button1_Click(Object sender, EventArgs e)
在 System.Windows.Forms.Control.OnClick(EventArgs e)
在 System.Windows.Forms.Button.OnClick(EventArgs e)
在 System.Windows.Forms.Button.OnMouseUp(MouseEventArgs 事件)
在 System.Windows.Forms.Control.WmMouseUp(消息和 m,MouseButtons 按钮,Int32 点击)
在 System.Windows.Forms.Control.WndProc(Message&m)
在 System.Windows.Forms.ButtonBase.WndProc(Message&m)
在 System.Windows.Forms.Button.WndProc(Message&m)
在 System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message&m)
在 System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
在 System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)

但是如果throw outer;BeginInvoke 调用中,如上面的代码,那么ThreadException 处理程序只有 会获得内部异常。在调用ThreadException 之前,外部异常被剥离,我得到的只是:

System.Exception:内部

(这里没有调用堆栈,因为 inner 从未被抛出。在一个更现实的示例中,我捕获了一个异常并将其包装以重新抛出,会有一个调用堆栈。)

如果我使用SynchronizationContext.Current.Post 而不是BeginInvoke,也会发生同样的事情:外部异常被剥离,ThreadException 处理程序只获取内部异常。

我尝试在外部包裹更多层的异常,以防它只是剥离最外层的异常,但它没有帮助:显然某处有一个循环在按照while (e.InnerException != null) e = e.InnerException; 的行做某事。

我使用BeginInvoke 是因为我的代码需要抛出一个未处理的异常以由ThreadException 立即处理,但此代码位于调用堆栈更高的catch 块内(特别是,它在Task 的操作中,Task 将捕获异常并阻止它传播)。我正在尝试使用BeginInvoke 来延迟throw,直到下一次在消息循环中处理消息,此时我不再在catch 中。我不依附BeginInvoke的具体解决方案;我只想抛出一个未处理的异常。

即使我在其他人的catch-all 中,我如何才能导致异常(包括其内部异常)到达ThreadException

(由于程序集的依赖性,我不能直接调用我的ThreadException-handler 方法:处理程序被 EXE 的启动代码挂钩,而我当前的问题是在较低层的 DLL 中。)

【问题讨论】:

  • 是的,这是故意的。 Control.InvokeMarshaledCallbacks() 捕获任何异常,以便将其封送回调用者。使用 Invoke() 或 EndInvoke() 时相关。它使用 Exception.GetBaseException()。你无法解决这个问题。但是,您可以自己捕获异常并在这种特定情况下直接调用您的事件处理程序。
  • @HansPassant,我看到调用 GetBaseException 的 InvokeMarshaledCallbacks 中的代码。由于 GetBaseException 是虚拟的,这表明我可以覆盖它。不幸的是,我的重写 GetBaseException 方法从未被调用——所以我不确定这是导致问题的特定代码。
  • @Joe,使用 GetBaseException 对我有用。
  • @HansPassant,你知道为什么 Control.InvokeMarshaledCallbacks() 会吃掉外部异常吗?
  • @HansPassant calling Exception.GetBaseException() here 有什么意义?它没有提供任何好处,它只会减少人们在显示的未处理异常对话框中看到的上下文/堆栈的数量,并防止任何ThreadException 处理程序看到引发的实际(外部)异常。既然这是故意的,那么人们试图通过这样做来解决什么问题?不能修吗?

标签: c# exception unhandled-exception


【解决方案1】:

将异常传播到更高层的推荐方法(除了通过等待任务隐式重新抛出)是删除任务主体中的全部内容,而是使用Task.ContinueWith在任务上注册故障延续,指定TaskContinuationOptions.OnlyOnFaulted。如果您正在通过中间层工作并且无权访问任务,则可以进一步将其包装在您自己的 UnhandledException 事件中以向上传递 Exception 对象。

【讨论】:

  • 当然。但我希望异常最终出现在我的 ThreadException 处理程序中,这意味着 UI 线程需要在我的 Task 上调用 Wait (或者在 UI 线程上重新抛出异常)。我的实际代码是在 BeginInvoke 中执行 WaitAll (为了简单起见,我的示例代码只是显示了一个 throw)。 WaitAll 抛出带有多个 InnerExceptions 的 AggregateException,但由于钻入 InnerException 行为,我只看到一个内部异常,而不是完整的 AggregateException。
【解决方案2】:

一种方法是将内部异常引用放入自定义属性或Data 字典中——即,将InnerException 属性保留为空,并以其他方式携带该引用。

当然,这需要建立某种可以在抛出代码和处理代码之间共享的约定。最好的方法可能是在两个代码都引用的项目中定义一个具有自定义属性的自定义异常类。

示例代码(尽管它需要更多的 cmets 来解释它为什么会做疯狂的事情):

public class ExceptionDecorator : Exception {
    public ExceptionDecorator(Exception exception) : base(exception.Message) {
        Exception = exception;
    }
    public Exception Exception { get; private set; }
}

// To throw an unhandled exception without losing its InnerException:
BeginInvoke(new Action(() => { throw new ExceptionDecorator(outer); }));

// In the ThreadException handler:
private void OnUnhandledException(object sender, ThreadExceptionEventArgs e) {
    var exception = e.Exception;
    if (exception is ExceptionDecorator)
        exception = ((ExceptionDecorator) exception).Exception;
    // ...
}

【讨论】:

【解决方案3】:

我假设您在 x64 Windows 系统上看到了这种行为,这是一个 - 相当未知 - x64 Windows 的实现细节。阅读它here

这篇文章详细介绍了如何通过应用一些修补程序来解决这个问题,据称这些修补程序是随 Win7 SP1 一起提供的,但几周前我在 Win7 SP1 上遇到了这个问题。

此外,您还可以附加到 AppDomain.FirstChanceException event,这使您可以在将每个异常传递给 CLR 进行处理之前访问它

【讨论】:

  • 这台机器确实在运行 Windows 7 x64。该应用程序是一个 32 位进程,但快速浏览文章表明这​​可能无关紧要。现在给它一个更深入的阅读......
  • 我不认为这是问题所在。 is 例外——它并没有像文章所说的那样被完全阻止。只是 .NET 中的某些东西只是传递给我最里面的异常,而不是实际的异常。
  • @JoeWhite 好的。无论如何,这是一个疯狂的猜测。最后一段对您有任何帮助吗?如果没有,我将删除我的答案
  • 我不知道 FirstChanceException 事件,所以你至少教会了我一些新东西。但由于每次抛出异常时都会触发它,无论是否处理,我真的看不到在这里使用它的好方法。
猜你喜欢
  • 2018-12-23
  • 1970-01-01
  • 1970-01-01
  • 2020-09-27
  • 2022-01-01
  • 2021-01-15
  • 2012-09-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多