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

时间:2015-06-09 09:51:43

标签: c# asynchronous exception-handling backgroundworker

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

短版

为什么ExceptionRunWorkerCompleted事件抛入调用代码未被捕获?

详细版本

  • 我有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 个答案:

答案 0 :(得分:3)

  

...没有被调用代码捕获?

"调用代码"是您的问题中的重要细节,谁完全调用您的RunWorkerCompleted事件处理程序?你知道它不能是你的代码,你从来没有写任何明确调用事件处理程序的代码。你所做的就是订阅活动。所以你知道那里有麻烦,因为它不是你的代码,你不可能编写一个try / catch来捕获异常并处理它。

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

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

常见的情况是Winforms,您在“调用堆栈”窗口中看到的与代码有关的最后一个语句是Main()方法中对Application.Run()的调用。您的事件处理程序是由对Control.BeginInvoke()的调用触发的,您无法看到它,但这是您的事件处理程序代码最终在UI线程上运行的方式。 Winforms在其Run()方法中有一个backstop catch语句捕获并引发Application.ThreadException事件。它仅在您不使用调试器时才有效。如果您没有订阅自己的事件处理程序,则默认处理程序将显示ThreadExceptionDialog对话框,该对话框为用户提供单击“继续”或“退出”的选项。让它远远达不到这个好主意,除了通过反复试验之外,你的用户没有办法选择正确的选择。请注意调试器所扮演的角色,如果没有调试器,它将以不同的方式工作,并且直接引发ThreadException事件。

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

因此,一般来说,有些不可避免的结果是你的应用程序将在重新引发异常时终止。没有任何仙女教母想要处理你不想处理的问题。处理这样的异常通常是相当可疑的,你几乎不知道你的DoWork事件处理程序究竟出了什么问题。它是一个给定的,它无法处理异常,否则它会抓住它。你可以在以后做这件事并且正确地做到这一点的可能性很快就会从投掷的声明中删除掉。

你在RunWorkerCompleted事件处理程序中真正知道的是"它不起作用"。处理此类异常是有风险的,您不知道有多少程序状态被失败的代码变异。但是有一个很大的优势,你在DoWork中提供的代码本质上是非常松散耦合的。非常重要的是,在工作线程上运行的紧密耦合的代码几乎不可能编写,您很少能够正确地获得所需的锁定。

在实践中,您执行的操作根本不会改变任何状态。就像使用查询结果填充列表的dbase查询一样。您可以在事件处理程序中使用e.Result属性来应用后台操作的结果。因此,当它不起作用时没有真正的伤害,你只是没有得到结果。你必须让用户知道它,毕竟他没有得到他预期的结果。通常,用户必须打电话给某人才能解决问题,希望不是你,而是IT人员解决了潜在的问题。 MesssageBox.Show()完成了这项工作。

答案 1 :(得分:2)

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

RunWorkerAsync()(正如它明确指出的)是一种异步方法,因此它不会 阻止 等待< / EM> 即可。这意味着主线程可以自由继续,因此即使BGW DoWork可能仍然忙碌,也会退出try / catch。

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

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