为什么在BackgroundWorker的RunWorkerCompleted事件中不能重新抛出异常?

3
我已经查看了一些关于如何解决此问题的帖子,但我无法完全理解这种异常行为的原因。
简短版本:
为什么在 RunWorkerCompleted 事件中抛出的 Exception 不会被调用代码捕获?
详细版本:
- 我有一个 BackgroundWorker(以下简称 BGW)。 - BGW 的 DoWork 事件抛出一个 Exception。 - BGW 的 RunWorkerCompleted 事件捕获该 Exception,并记录并进行一些很酷的清理工作。 - 在清理后,RunWorkerCompleted 事件重新抛出了该 Exception。 如果 RunWorkerCompleted 事件在主线程上运行,那么这不意味着调用代码(也在主线程上)应该能够捕获该异常吗? - 一些代码来强化这个概念...
private void SomeMethod()
{
    BackgroundWorker bgw = new BackgroundWorker();
    bgw.DoWork += bgw_DoWork;
    bgw.RunWorkerCompleted += bgw_RunWorkerCompleted;
    bgw.RunWorkerAsync();
}

private void bgw_DoWork(object sender, DoWorkEventArgs e)
{
    throw new Exception("Oops");
}

private void bgw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if(e.Error != null)
    {
        // Log exception & cleanup code here...
        throw e.Error; // Always unhandled :(
    }
}

我认为像这样调用SomeMethod会捕获Exception并显示消息框,但是BGW的行为与我预期的不同,Exception始终未处理...
try
{
    SomeMethod();
}
catch (Exception)
{
    MessageBox.Show("Handled exception");
}
2个回答

3
“Calling code”是你问题中的重要细节,究竟是谁精确地调用了你的RunWorkerCompleted事件处理程序?你知道它不可能是你的代码,因为你从未编写过任何显式调用事件处理程序的代码。你所做的一切就是订阅事件。因此,你知道有麻烦即将来临,因为它不是你的代码,你不可能编写try/catch来捕获并处理异常。
我强烈建议你只需查看一下,在你的事件处理程序上设置断点,当它停止时,查看调试器的“调用堆栈”窗口,看看它是如何到达那里的。请记住,这将是.NET Framework代码,你需要关闭“仅我的代码”调试器选项,以便你可以看到所有内容。
首先是不寻常的情况,在控制台应用程序或服务中,是SychronizationContext.Post()调用了事件处理程序。代码在任意线程池线程上运行,并且没有任何catch语句可以捕获此异常。你的应用程序将始终终止。
一个常见的情况是Winforms,在调用Main()方法中看到与你的代码有关的最后一个语句是对Application.Run()的调用。你的事件处理程序是通过对Control.BeginInvoke()的调用触发的,你看不到它,但这就是你的事件处理程序代码如何在UI线程上运行的。Winforms在其Run()方法中有一个后备catch语句,它会捕获并引发Application.ThreadException事件。只有当你不使用调试器时才会激活它。如果你没有订阅自己的事件处理程序,那么默认处理程序将显示ThreadExceptionDialog对话框,该对话框为用户提供了单击“继续”或“退出”的选项。永远不要让它走到这一步,你的用户没有合适的方法来选择正确的选项,除非通过试错。请注意调试器所起的作用,没有调试器会有不同的工作方式,并且ThreadException事件会直接引发。
下一个常见情况是WPF,它与Winforms非常相似,你的事件处理程序是由对Dispatcher.BeginInvoke()的调用触发的。它也在其分派程序循环中具有catch语句。类似地,它引发了DispatcherUnhandledException事件。它没有像Winforms那样针对该事件的默认处理程序,如果你没有订阅处理程序,则应用程序将终止。
因此,总的来说,当重新引发异常时,你的应用程序将不可避免地终止。没有任何仙女愿意处理你不想处理的问题。通常情况下,像这样处理异常是相当可疑的,因为你几乎不知道在DoWork事件处理程序中实际发生了什么错误。可以肯定的是,它无法处理异常,否则它就会捕获它。随着catch语句距离引发异常的语句越远,你能够稍后正确地处理异常的可能性就会迅速减少。
在RunWorkerCompleted事件处理程序中,你只知道“它没有起作用”。处理这样的异常是有风险的,因为你不知道失败代码修改了多少程序状态。但是,DoWork中所放置的代码通常都是非常松散耦合的,这是一个很大的优势。在工作线程上运行非常重要的紧密耦合的代码几乎是不可能的,你很难得到所需的锁定。
在实践中,你执行的单个操作根本不会改变任何状态,例如填充包含查询结果的列表的数据库查询。你可以使用事件处理程序中的e.Result属性来应用后台操作的结果。因此,如果操作没有成功,就不会造成实质性的损失,你只是没有得到结果。然而,你必须告诉用户这一点,毕竟他没有得到预期的结果。并且通常情况下,用户需要打电话给某个人来纠正问题,希望不是你,而是IT工作人员来修复底层问题。MessageBox.Show()可以完成这项工作。

2

我在校对自己的问题时想到了答案,现在分享给大家,希望能帮助到其他人。答案存在于RunWorkerAsync()方法和我的一些错误理解之间。

RunWorkerAsync()是一个异步方法,正如它清楚地说明的那样,因此它不会阻塞等待。这意味着主线程可以继续执行,因此即使BGW的DoWork仍然忙碌,主线程也会退出try/catch块。

当BGW的DoWork最终抛出异常时,RunWorkerCompleted事件不再在调用代码的try/catch范围内。

在调用代码中捕获它实际上没有意义。允许调用者捕获它会导致不稳定的竞争条件,有时它会被捕获,有时则为时已晚而未处理。


谢谢!今天我在这个问题上感到非常困惑。现在我想起来,答案似乎很明显。 - thephez

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