【问题标题】:Aren't destructors guaranteed to finish running?析构函数不能保证完成运行吗?
【发布时间】:2012-04-14 01:30:17
【问题描述】:

析构函数是奇怪的。我试图通过使用“智能”引用管理来消除使用一次性模式的需要,确保垃圾收集器可以在正确的时间收集对象。在我的一个析构函数中,我不得不等待来自另一个对象的事件,我注意到它没有。应用程序简单地关闭,析构函数在执行过程中终止。 我希望总是允许析构函数完成运行,但以下测试表明这不是真的。

using System;
using System.Diagnostics;
using System.Threading;


namespace DestructorTest
{
    class Program
    {
        static void Main( string[] args )
        {
            new DestructorTest();
            new LoopDestructorTest();
            using ( new DisposableTest() ) { }
        }
    }

    class DestructorTest
    {
        ~DestructorTest()
        {
            // This isn't allowed to finish.
            Thread.Sleep( 10000 );
        }       
    }

    class LoopDestructorTest
    {
        ~LoopDestructorTest()
        {           
            int cur = 0;
            for ( int i = 0; i < int.MaxValue; ++i )
            {
                cur = i;
            }
            // This isn't allowed to finish.
            Debug.WriteLine( cur );
        }
    }

    class DisposableTest : IDisposable
    {
        public void Dispose()
        {
            // This of course, is allowed to finish.
            Thread.Sleep( 10000 );
        }
    }
}

那么,析构函数不能保证完成运行吗?

【问题讨论】:

  • 在终结器中等待来自另一个对象的事件?这太疯狂了!
  • 您确实应该坚持使用IDisposable 模式,以获得可靠的销毁和资源释放。
  • @MattDavey ... 并等待来自另一个对象的事件继续触发Dispose() 不是疯狂吗?有什么区别?
  • link阅读The Finalize method might not run to completion or might not run at all in the following exceptional circumstances:下的第二个项目符号

标签: c# destructor finalizer


【解决方案1】:

那么,析构函数不能保证完成运行吗?

没有。据我记得,当进程终止时,它会给终结器几秒钟的时间来执行,但随后会突然终止进程。您不会想要一个糟糕的终结器来阻止进程完成,对吗?

您应该将最终确定视为“尽力而为”的清理 - 特别是,它不会在整个系统突然关闭的情况下发生,例如 BSOD 或断电。

编辑:我发现了一些blog post from Joe Duffy 形式的伪文档:

如果一个锁在停止所有正在运行的线程的过程中被孤立,那么关闭代码​​路径将无法获取锁。如果这些获取是通过非超时(或长时间超时)获取完成的,则会出现挂起。为了应对这种情况(以及可能发生的任何其他类型的挂起),CLR 指定了一个看门狗线程以密切关注终结器线程。尽管可配置,但默认情况下,CLR 会让终结器运行 2 秒,然后变得不耐烦;如果超过此超时,终结器线程将停止,并继续关闭而不耗尽终结器队列的其余部分。

【讨论】:

  • 实际上我会,或者至少我想在 Visual Studio 中得到一个巨大的警告,应用程序没有正确完成(因为并不是所有的析构函数都被允许完成运行)。
  • @StevenJeuris:听起来你在真正不应该的情况下依赖终结器。终结器应该帮助避免资源泄漏——如果你的系统没有运行到完成就会进入一个糟糕的状态,你应该重新设计。毕竟,由于 BSOD、停电等原因,它们总是有可能无法运行
  • 好吧,重新设计当然涉及使用IDisposable,它不会突然终止,因为它必须等待 x ms 的事件表明处理步骤已完成并且可以执行下一步。我只是没有看到析构函数的意义。在什么情况下应该依赖它们?
  • @StevenJeuris:依靠它们始终执行,以确保在您的进程之外的持久状态的正确性?从来没有,就我而言。您应该始终考虑到您的系统可能会简单地停止执行。您可以依靠它们来清理进程中不需要急切释放的资源。就我个人而言,我不记得我上次写终结器是什么时候了……
  • 我不关心系统故障,持久性也不关心。我只是试图清理非托管内存而不必担心何时(例如using 或调用Dispose())。您的回答清楚地说明了为什么总是必须通过IDisposable 发生。它还清楚地表明,即使在析构函数中调用 Dispose() 是模式建议的内容,它也只是一个微不足道的最后手段。我不想“急切地”释放那些非托管资源,我只想在 GC 想要的时候释放它们。谢谢你的解释!我也不会再使用它们了。
【解决方案2】:

那么,析构函数不能保证完成运行吗?

虽然它不在您的代码中,但在某些情况下,可能会从 Dispose() 方法显式调用 GC.SuppressFinalize。这会抑制最终确定,并且适用于不需要它的对象。

这可以显着提高性能,因为可终结对象将始终在一次垃圾回收中幸存下来,即它将被提升为 gen1 甚至是 gen2 GC,这会带来更高的成本。

【讨论】:

    【解决方案3】:

    .NET 不附带析构函数。您的代码包含 finalizers

    在对象被垃圾回收时调用终结器,而不是在对象被无效时调用。它们还获得有限的执行时间以防止悬挂物体。

    另见https://en.wikipedia.org/wiki/Finalizer

    【讨论】:

    • 但是 C# 确实有 destructors
    • 编写 C# 规范的人已经规定,以波浪号后跟类名的语法元素被正式称为析构函数。我碰巧认为析构函数的语法和术语都很愚蠢,因为(1)“析构函数”这个词已经被其他语言使用,表示不同且不相关的东西; (2) 术语“终结器”存在并描述了 C# 析构函数通常试图做的事情,尽管析构函数在其他代码中包装了终结器; (3)正确使用析构函数一般需要GC.SuppressFinalize()GC.KeepAlive()....
    • ...所以除非存在支持后两个功能的框架,但通过覆盖Object.Finalize以外的其他方法执行终结,使用析构函数并不比覆盖Object.Finalize更便携, 如果 C# 允许后一种行为。
    【解决方案4】:

    另一种看待方式是当垃圾收集器释放内存时调用终结器。

    但是,如果您有一个最多需要 1Mb 内存的程序,但在具有 10Mb 内存的机器上运行,那么抓取收集器的有效实现将是什么都不做(因为有足够的内存供程序执行)。在这种情况下,将永远不会调用终结器。

    【讨论】:

      猜你喜欢
      • 2016-07-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-05-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多