finally块在.NET 4.0上没有运行,为什么?

20

好的,这是一个有点奇怪的问题,我希望有人能够解决。我有以下代码:

static void Main(string[] args)
{
    try
    {
        Console.WriteLine("in try");
        throw new EncoderFallbackException();
    }
    catch (Exception)
    {
        Console.WriteLine("in Catch");
        throw new AbandonedMutexException();
    }
    finally
    {
        Console.WriteLine("in Finally");
        Console.ReadLine();
    }
}

现在当我将其编译为目标3.5(2.0 CLR)时,它会弹出一个窗口显示“XXX已停止工作”。如果我现在点击取消按钮,它将运行finally,如果我等待它完成查找并点击关闭程序按钮,它也将运行finally

现在有趣且令人困惑的是,如果我使用4.0进行相同的操作,点击取消按钮将运行finally块,而点击关闭程序按钮则不会。

我的问题是:为什么在点击关闭程序按钮时,2.0版本会运行finally,而4.0版本不会?这会带来什么后果?

编辑:我在Windows 7 32位上以发布模式(内置发布模式)从命令提示符中运行此代码。错误消息:第一个结果是在3.5上运行,关闭程序后Windows查找问题,第二个结果是在4.0上运行并执行相同操作。

Screenshot


4
当我使用.NET 4编译并运行它时,会弹出标准的Windows错误报告对话框;如果我关闭它或点击"不发送",则控制台会输出"in Finally"。看起来工作正常! - Graham Clark
1
你是在Visual Studio中运行吗?尝试从命令提示符手动运行它。 - Graham Clark
64/32位系统之间也可能存在差异。 - Kobi
1
我怀疑这可能与操作系统处理崩溃应用程序的设置有关。 - Dirk Vollmar
1
对我也有效 - Windows 2003,x84。 - Kobi
显示剩余4条评论
5个回答

21

我现在能够重现这个问题了(当我第一次阅读您的问题时,我没有得到确切的步骤)。

我可以观察到的一个区别是.NET运行时处理未处理异常的方式不同。CLR 2.0 运行一个名为 Microsoft .NET Error Reporting Shim (dw20.exe) 的帮助程序,而 CLR 4.0 则启动 Windows Error Reporting (WerFault.exe)。

我认为这两者在终止崩溃进程方面有不同的行为。显然,WerFault.exe 立即终止.NET进程,而.NET Error Reporting Shim 以某种方式关闭应用程序,以便仍会执行 finally 块。

同时,请查看事件查看器:WerFault 记录了一个应用程序错误,通知已终止崩溃的进程:

Application: ConsoleApplication1.exe
Framework Version: v4.0.30319
Description: The process was terminated due to an unhandled exception.
Exception Info: System.Threading.AbandonedMutexException
Stack:
   at Program.Main(System.String[])

然而,dw20.exe 只向事件日志记录了一个具有事件 ID 1001 的信息项,并未终止该进程。


10

考虑一下这种糟糕的情况:发生了意料之外的事情,没有人编写代码来处理。在这种情况下,运行更多代码是正确的做法吗?那些可能也没有为这种情况建立起来?可能不是。在这里通常正确的做法是尝试不要运行 finally块,因为这样做会使糟糕的情况变得更加糟糕。你已经知道进程正在崩溃;立即结束它的痛苦。

在即将导致进程崩溃的情况下,任何事情都可能发生。在这种情况下,该怎么做是由具体实现定义的:是否向Windows错误报告报告错误,是否启动调试器等等。CLR完全有权利尝试运行finally块,并且完全有权利快速失败。在这种情况下,所有的赌注都被关掉了。不同的实现可以选择不同的操作。


7

我对这个主题的所有知识都来自于这篇文章:CLR Inside Out - Unhandled Exception Processing In The CLR。请注意,它是针对.NET 2.0编写的,但我有一种感觉,它对我们在这种情况下遇到的问题是有意义的(无论如何,不只是“因为它决定这样做”)。

快速回答“我没有时间阅读那篇文章”(尽管你应该阅读,它真的很好):

解决问题的方法(如果您绝对必须运行finally块)是a)放置全局错误处理程序或b)强制.NET始终运行finally块,并以.NET 1.1中(可以说是错误的)方式执行操作-将以下内容放入您的app.config中:

<legacyUnhandledExceptionPolicy enabled="1">

原因是这样的: 当在.NET中抛出异常时,它会开始沿着堆栈向后查找异常处理程序,当找到一个处理程序时,它会再次沿着堆栈向后运行finally块之前运行catch的内容。如果没有找到catch,则不会发生第二次遍历,因此在这里finally块永远不会运行,这就是为什么全局异常处理程序总是在CLR找到catch时运行finally子句的原因(我相信即使你使用catch/throw,finally块仍然会被执行)。
app.config修复的原因是因为对于.NET 1.0和1.1,CLR中有一个全局catch,它会在未经管理的情况下吞噬异常,这当然会触发finally块的运行。当然,框架无法对该异常有足够的了解来处理它,例如堆栈溢出,所以这可能是错误的做法。
接下来的部分有点棘手,我根据文章的说法做了一些假设。

如果你在.NET 2.0+中没有使用传统的异常处理,那么你的异常会落入Windows异常处理系统(SEH)中,这似乎与CLR非常相似,因为它会回溯帧直到找不到catch,然后调用一系列称为未处理异常过滤器(UEF)的事件。这是一个可以订阅的事件,但它一次只能有一个订阅对象,因此当某个东西订阅时,Windows会将之前存在的回调地址交给它,允许你设置一系列UEF处理程序链 - 但它们不必遵守该地址,它们应该自己调用该地址,但如果其中一个打破了链,你就无法再进行错误处理了。我认为当你取消Windows错误报告时,这就是发生的事情,它会打破UEF链,这意味着应用程序立即关闭并且finally块不会运行,但是如果你让它运行到结束并关闭它,它将调用链中的下一个UEF。.NET将注册一个,这就是AppDomain.UnhandledException被调用的地方(因此即使这个事件也不能保证),我认为这也是你从中调用finally块的地方 - 因为我看不出如果你从未回到CLR中,管理的finally块如何运行(文章没有涉及到这一点)。


3

我相信这与调试器附加方式的更改有关。

来自.NET Framework 4 Migration Issues文档:

当调试器无法启动或没有注册应该启动的调试器时,您将不再收到通知。

发生的情况是您选择启动调试器,但取消了它。我认为这属于此类别,应用程序因此停止运行。


问题似乎发生在没有任何调试器的情况下。 - Dirk Vollmar
谢谢你的信息,我不知道那个,但我只是从命令提示符中运行它。 - jquery auth
错误信息显示行号,建议您在同一目录下拥有pdb文件。我不确定它是否重要,无论如何我都无法复制它。 - Kobi
1
是的,在.NET 4.0中取消JIT调试器对话框会强制终止程序。 - Hans Passant
@Hans - 我没有任何取消选项,但我可以选择调试,然后选择,它会终止程序。 - Kobi

0
在发布版和调试版中都运行了这个程序,在框架3.5和4.0中都测试过,我在所有情况下都看到了“在Finally”,是的,我是从命令行运行的,甚至关闭了我的VS会话,也许是你的机器上的问题,或者正如Kobi指出的那样,可能与平台有关(我使用的是Win7 x64)。

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