为什么 finally 块中的代码不会执行?

9

似乎在除主线程以外的线程执行代码时,finally块不会执行。那么有没有可能强制执行finally块呢?

环境:VS 2010,.Net Framework 4.0.3

class Program
{
    static void Main(string[] args)
    {
        var h = new AutoResetEvent(false);

        ThreadPool.QueueUserWorkItem(
            obj => TestProc(h));

        h.WaitOne();
    }

    private static void TestProc(EventWaitHandle h)
    {
        try
        {
            Trace.WriteLine("Try");
            h.Set();
        }
        catch(Exception)
        {
            Trace.WriteLine("Catch");
        }
        finally
        {
            Thread.Sleep(2000);
            Trace.WriteLine("Finally");
        }
    }
}

更新:

我在MSDN中找到了有关该案例的提及和解释:

ThreadAbortException类 http://msdn.microsoft.com/zh-cn/library/system.threading.threadabortexception.aspx

当调用Abort方法来销毁线程时,公共语言运行时会抛出一个ThreadAbortException。ThreadAbortException是一种特殊的异常,可以捕获,但它会自动在catch块结束时重新引发。当引发此异常时,运行时将执行所有finally块后结束线程。因为线程可以在finally块中进行无限计算或调用Thread.ResetAbort取消中止,所以不能保证线程将结束。如果要等待已中止的线程结束,可以调用Thread.Join方法。Join是一个阻塞调用,只有在线程实际停止执行时才返回。

注意:

当公共语言运行时(CLR)在托管可执行文件的所有前台线程结束后停止后台线程时,不使用Thread.Abort。因此,无法使用ThreadAbortException检测CLR终止后台线程的情况。


前台线程与后台线程 http://msdn.microsoft.com/zh-cn/library/h339syd0.aspx

当运行时停止后台线程,因为进程正在关闭,线程中不会抛出异常。然而,当使用AppDomain.Unload方法卸载应用程序域时,前台线程和后台线程都会引发ThreadAbortException。


那么为什么在应用程序的结束(kill)之前CLR不使用AppDomain.Unload方法来卸载应用程序域呢? 因为http://msdn.microsoft.com/zh-cn/library/system.appdomain.unload.aspx

当线程调用Unload时,目标域将被标记为要卸载。专用线程尝试卸载域,并终止域中的所有线程。如果线程没有终止,例如因为它正在执行非托管代码或因为它正在执行finally块,则在一段时间后会在最初调用Unload的线程中引发CannotUnloadAppDomainException。如果无法终止的线程最终结束,则不会卸载目标域。因此,在.NET Framework版本2.0中,不能保证域已卸载,因为可能无法终止执行线程。

结论:在某些情况下,我需要考虑我的代码是否将在后台或前台线程中执行?我的代码是否可能在应用程序主线程结束所有工作之前未完成?

4
您的程序在睡眠完成之前就退出了。它确实在运行……只是被中断了。 - Andrew Barber
3个回答

17

您的代码运行在后台线程中。当您设置AutoResetEvent时,单个前台线程终止(因为您已经到达Main方法的结尾),进程“立即”被关闭。

实际上,我认为您的finally块将会开始执行,但由于您首先执行了两秒的休眠操作,在到达WriteLine调用之前进程就退出了。

如果您的Main方法仍在运行,或者任何其他前台线程保持进程活动状态,您将看到finally块像正常一样完成。这不是“在其他线程上使用finally”的问题 - 而是“只有在存在前台线程时进程才会保持活动状态”的问题。


我认为在编写那段代码时我理解了,但在我看来这是正常操作,并且finally块保证在这种情况下执行。这是正常的执行流程,但最终被中止了。这对我来说是令人惊讶的行为。 - AndreyR
1
@Andir:为什么这让你惊讶?你要求在后台线程上运行代码,所以它就这样做了。如果你不想这样,那就使用前台线程。坦白地说,如果你在后台线程上运行代码并且它阻止应用程序退出,那才是真正让人惊讶的事情。这不是后台线程应该具有的含义。 - Mark Byers
一个线程无论是否有finally块,如果你终止了它,它就不能运行任何代码。如果你想在终止线程之前等待它完成,你必须编写相应的代码。 - David Schwartz
@Andir:嗯,就像我说的,我怀疑它开始执行...但是并没有执行得很远。(我在控制台应用程序中尝试了一下,在finally块的开头加了一个Console.WriteLine,在休眠之前,它显示了那个消息。如果这对你来说都是令人惊讶的行为,那可能只是因为你不知道进程终止策略。你期望一个在无限循环中的后台线程做什么?永远保持进程活动吗?如果是这样的话,前台线程和后台线程有什么区别呢? - Jon Skeet
2
@Andir:无论如何,您始终需要应对进程突然崩溃的可能性。请注意,如果需要,可以添加替代关闭挂钩。 - Jon Skeet
显示剩余2条评论

4

您可以通过多种方法防止主方法在finally执行前退出。

  • You can use synchronization to achieve this. For example using a ResetEvent, similar to what you are already doing, or creating a thread explicitly and joining with it.

  • You could just a simple sleep or readline at the end of the Main method:

    h.WaitOne();
    Console.ReadLine();
    

然后用户可以控制程序何时退出。

  • 您可以使用非后台线程而不是线程池中的线程。然后,程序将在线程终止之前不会退出。如果您希望程序在线程完成之前不终止,这可能是最好和最简单的选择。

0

我也曾经遇到过相同的问题,在尝试了多种方法之后,以下方法有效:参考链接https://learn.microsoft.com/en-us/visualstudio/code-quality/ca2124?view=vs-2019

本质上就是在你的Main()函数里加入下面的代码。我一直在使用它,可以像预期一样整理好我的代码。

try { try {} finally {} } catch {}

那样,你的最终清理代码将运行,并且仍然可以捕获任何异常。

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