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

Why can an Exception not be rethrown in the BackgroundWorker RunWorkerCompleted event

我查看了一些关于这个主题的帖子,其中涵盖了如何解决这个问题,但我不太明白为什么 这种奇怪的行为?

短版:

为什么在 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");
}

我在校对自己的问题时弄明白了。无论如何我都会分享,希望能帮助别人。答案介于 RunWorkerAsync() 方法和我愚蠢的误解之间。

RunWorkerAsync() 是(正如它明确指出的)异步方法,因此它不会 block等等。这意味着即使 BGW 的 DoWork 可能仍然很忙,主线程仍可以继续并因此退出 try/catch。

当 BGW 的 DoWork 最终抛出 Exception 时,RunWorkerCompleted 事件不再在调用代码的 try/catch.

范围内

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

... not caught by the calling code?

"Calling code" 是您问题中的重要细节,谁 确切地 调用了您的 运行WorkerCompleted 事件处理程序?您知道它不可能是您的代码,您从未编写过任何显式调用事件处理程序的代码。您所做的只是订阅该事件。所以你知道那里有麻烦,因为它不是你的代码,你不可能写一个 try/catch 来捕获那个异常并处理它。

我强烈建议您看一下,在您的事件处理程序上设置一个断点,当它中断时,查看调试器的调用堆栈 window 以了解它是如何到达那里的。请记住,这将是 .NET Framework 代码,您需要关闭“仅我的代码”调试器选项,以便您可以看到所有内容。

首先是不常见的情况,在控制台应用程序或服务中,是 SychronizationContext.Post() 调用了事件处理程序。代码在任意线程池线程上运行,并且没有任何 catch 语句可以捕获此异常。您的应用程序将始终终止。

常见情况是 Winforms,您在调用堆栈 window 中看到的最后一条与您的代码有关的语句是在 Main() 方法中调用 Application.Run() .您的事件处理程序是由对 Control.BeginInvoke() 的调用触发的,您看不到它,但这就是您的事件处理程序代码在 UI 线程上结束 运行 的方式。 Winforms 在其 运行() 方法中有一个 backstop catch 语句,该语句会捕获并引发 Application.ThreadException 事件。它仅在您不使用调试器时处于活动状态。如果您没有以其他方式订阅自己的事件处理程序,则默认处理程序会显示 ThreadExceptionDialog 对话框,该对话框使用户可以选择单击“继续”或“退出”。让它走那么远不是一个好主意,除了反复试验之外,您的用户没有合适的方法来选择正确的选择。请注意调试器扮演的角色,如果没有调试器,它的工作方式会有所不同,并且会直接引发 ThreadException 事件。

下一个常见情况是 WPF,它的工作方式与 Winforms 非常相似,您的事件处理程序是通过调用 Dispatcher.BeginInvoke() 触发的。它的调度程序循环中也有一个 catch 语句。同样,它引发 DispatcherUnhandledException 事件。它 没有 像 Winforms 那样有一个默认的事件处理程序,如果你不订阅一个,那么应用程序将终止。

因此,一般来说,不可避免的结果是您的应用程序将在重新引发异常时终止。周围没有任何神仙教母愿意处理你不想处理的问题。像这样处理异常通常是有问题的,您几乎不知道 DoWork 事件处理程序中到底出了什么问题。假设它无法处理异常,否则它会捕获它。从抛出的语句中删除捕获物的距离越远,您以后可以做并正确做的几率就会迅速下降。

您在 运行WorkerCompleted 事件处理程序中真正知道的是 "it did not work"。处理此类异常是有风险的,您不知道有多少程序状态被失败的代码改变了。不过有一个很大的优势,你放在 DoWork 中的代码本质上是非常松散耦合的。非常重要,在工作线程上运行的紧耦合代码几乎不可能编写,您很少能正确获得所需的锁定。

实际上,您执行的是一个操作,根本不会改变任何状态。就像一个用查询结果填充列表的数据库查询。您在事件处理程序中使用 e.Result 属性 来应用后台操作的结果。所以当它不起作用时并没有真正的伤害,你只是没有得到结果。你一定要让用户知道,毕竟他没有得到他期望的结果。并且用户经常不得不打电话给某人来纠正问题,希望不是你,而是修复潜在问题的 IT 人员。 MesssageBox.Show() 完成了这项工作。