【问题标题】:Which objects can I use in a finalizer method?我可以在终结器方法中使用哪些对象?
【发布时间】:2011-08-06 01:52:34
【问题描述】:

我有一个类在处理或完成时应该删除一些文件。在终结器中,我不能使用其他对象,因为它们可能已经被垃圾回收了。

我是否遗漏了关于可以使用终结器和字符串的一些要点?

UPD:类似的东西:

public class TempFileStream : FileStream
{
    private string _filename;

    public TempFileStream(string filename)
        :base(filename, FileMode.Open, FileAccess.Read, FileShare.Read)
    {
        _filename = filename;
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        if (_filename == null) return;

        try
        {
            File.Delete(_filename); // <-- oops! _filename could be gc-ed already
            _filename = null;
        }
        catch (Exception e)
        {
            ...
        }
    }
}

【问题讨论】:

  • 没有任何东西在使用或可以使用时被垃圾收集,即使在终结器中也是如此。
  • 不,@Dani 实际上是正确的。详情见我的回答。

标签: c# .net


【解决方案1】:

是的,您当然可以使用终结器中的字符串以及许多其他对象类型。

对于所有这一切的确切来源,我会去拿起 Jeffrey Richter 写的书CLR via C#, 3rd edition。在第 21 章中对此进行了详细描述。

无论如何,这就是真正发生的事情......

在垃圾回收期间,任何具有终结器但仍希望被调用的对象都被放置在一个特殊的列表中,称为 freachable 列表。

这个列表被认为是一个根,就像静态变量和实时局部变量一样。因此,那些对象引用的任何对象,以此类推,这一次都从垃圾回收周期中移除。 它们将在当前的垃圾收集周期中存活下来,就好像它们一开始就没有资格收集一样。

请注意,这包括字符串,这是您的问题,但它也涉及所有其他对象类型

然后,在稍后的某个时间点,终结器线程从该列表中获取对象,并对这些对象运行终结器,然后将这些对象从该列表中移除。

然后,下次垃圾回收运行时,它会再次找到相同的对象,但这一次终结器不再想运行,它已经执行,所以对象被正常回收。

在我告诉你什么不起作用之前,让我用一个例子来说明。

假设您有对象 A 到 Z,并且每个对象都引用下一个对象,因此您有对象 A 引用对象 B,B 引用 C,C 引用 D,依此类推,直到 Z。

其中一些对象实现了终结器,并且它们都实现了 IDisposable。让我们假设 A 没有实现终结器,但 B 实现了,然后其余的一些也实现了,对于这个超出 A 和 B 的示例来说并不重要。

您的程序保留了对 A 的引用,并且只有 A。

在一个普通且正确的使用模式中,您将处理 A,处理 B,处理 C,等等。但是您有一个错误,所以这不会发生。在某个时候,所有这些对象都有资格被收集。

此时 GC 会找到所有这些对象,但随后注意到 B 有一个终结器,它还没有运行。因此,GC 会将 B 放在 freachable 列表中,并递归地从 GC 列表中取出 C、D、E 等直到 Z,因为 B 突然变为 in- em> 有资格收集,其余的也是如此。请注意,其中一些对象本身也被放置在 freachable 列表中,因为它们自己有终结器,但所有它们引用的对象将在 GC 中存活。 p>

但是,A 被收集了。

让我把上面的段落说清楚。此时,A 已被收集,但 B、C、D 等直到 Z 还活着,好像什么都没发生过。虽然你的代码不再引用它们中的任何一个,但freachable列表有。

然后,终结器线程运行,并终结 freachable 列表中的所有对象,并将对象从列表中移除。

下次运行 GC 时,现在会收集这些对象。

所以这肯定行得通,那么大吵闹是怎么回事?

问题出在终结器线程上。该线程不假设它应该完成这些对象的顺序。它不这样做是因为在许多情况下它不可能这样做。

正如我上面所说,在一个普通的世界中,你会在 A 上调用 dispose,它会释放 B,它会释放 C,等等。如果这些对象之一是流,则引用流的对象可能会在调用 Dispose 时,说“我会在处理流之前继续刷新我的缓冲区。”这是完全合法的,很多现有的代码都是这样做的。

但是,在终结线程中,不再使用此顺序,因此如果将流放在引用它的对象之前的列表中,则流在引用它的对象之前被终结并因此关闭。

也就是说,你不能做的事情总结如下:

您无法访问对象引用的任何具有终结器的对象,因为您无法保证在终结器运行时这些对象将处于可用状态。 对象仍会在内存中那里,并且不会被收集,但它们可能已经被关闭、终止、终结等。

所以,回到你的问题

问。我可以在终结器方法中使用字符串吗?
A. 是的,因为字符串不实现终结器,并且不依赖具有终结器的其他对象,因此在终结器运行时将处于活动状态并开始运行。

让你走错路的假设是问题的第二句话:

在终结器中我不能使用其他对象,因为它们可能已经被垃圾回收了。

正确的句子是:

在终结器中我不能使用其他具有终结器的对象,因为它们可能已经被终结了。


举个例子,终结器无法知道正确终结两个对象的顺序,考虑两个相互引用并且都具有终结器的对象。终结器线程必须分析代码以确定它们通常以什么顺序被处理,这可能是两个对象之间的“舞蹈”。终结器线程不这样做,它只是在另一个之前终结一个,你无法保证哪个是第一个。


那么,是否有任何时候安全可以从我自己的终结器中访问也具有终结器的对象?

唯一保证安全的情况是当您的程序/类库/源代码拥有这两个对象时,您就知道它是。

在我解释之前,这不是很好的编程习惯,所以你可能不应该这样做

例子:

您有一个对象Cache,它将数据写入文件,该文件永远不会保持打开状态,因此只有在对象需要向其写入数据时才会打开。

您有另一个对象CacheManager,它使用第一个对象,并调用第一个对象为其提供数据以写入文件。

CacheManager 有一个终结器。这里的语义是,如果管理器类被收集,但没有被释放,它应该删除缓存,因为它不能保证它们的状态。

但是,缓存对象的文件名可以从缓存对象的属性中获取。

所以问题是,我是否需要将该文件名复制到管理器对象中,以避免在完成过程中出现问题?

不,你没有。当管理器最终确定时,缓存对象仍在内存中,它所引用的文件名字符串也是如此。但是,您不能保证缓存对象上的任何终结器尚未运行。

但是,在这种情况下,如果您知道缓存对象的终结器不存在,或者没有触及文件,那么您的管理器可以读取缓存的文件名属性对象,然后删除文件。

但是,由于您现在在这里有一个非常奇怪的依赖关系,我当然建议不要这样做。

【讨论】:

  • 很好的答案。我理解它的方式是终结器在 GC 之后运行,但是再次阅读本书的那部分,现在 GC 在之前和之后运行并且只在终结之后收集是有道理的。这样一来,复活及其相关的问题也很有意义。
  • 感谢您的精彩解释。我应该再读一遍里希特的书,因为我似乎忘记了一些事情:)
  • 确实是一个有价值的答案,但如果在顶部添加 TL;DR 将是完美的。据我了解,这将类似于“您应该避免从终结器调用引用的非原始对象的成员。从技术上讲,您可以安全地调用没有终结器的引用对象的成员,并且不要直接或间接使用具有终结器的对象。但请记住,实现是可以演变的,因此即使它在某些时候有效,它也可能成为以后难以调试的问题的根源。”
【解决方案2】:

还有一点还没有提到,尽管人们可能不会期望对象的终结器会在对象正在使用时运行,但终结机制并不能确保这一点。终结器可以在任意未知的线程上下文中运行;因此,他们应该避免使用任何不是线程安全的类型,或者应该使用锁定或其他方式来确保他们只以线程安全的方式使用事物。注意终结器应该使用Monitor.TryEnter 而不是Monitor.Enter,并且在意外持有锁时尽可能优雅地行事。请注意,由于终结器不应该在对象仍在使用时运行,因此意外持有锁的事实通常表明终结器已提前运行。根据使用锁的代码的设计,可以让终结器设置一个标志并再次尝试获取锁,并让任何其他使用锁的代码在释放它后检查是否设置了该标志和,如果是,则重新注册对象以进行最终确定。

在所有线程场景中正确处理终结清理是很困难的。终结器可能看起来并不复杂,但不存在方便的自动化机制来确保对象在使用相关对象时不会运行终结器。因此,终结器有很多微妙的线程安全问题。忽略此类问题的代码“通常”会工作,但有时可能会以难以诊断的方式失败。

【讨论】:

    【解决方案3】:

    您可以在终结器中调用 dispose 方法,并在 Dispose 方法中包含文件清理代码。除此之外,您还可以将布尔值传递给您的 dispose 方法,表明您正在从终结器调用它。

    有关正确使用 Dispose 和 Fianlizers 的出色参考,请阅读此Proper use of the IDisposable interface

    【讨论】:

    • 我知道标准模式,它不能回答我的问题。
    • @Ivan 对不起,我可能误解了你的问题。您可以发布突出显示您意图的代码吗?
    猜你喜欢
    • 1970-01-01
    • 2010-09-17
    • 1970-01-01
    • 1970-01-01
    • 2020-12-24
    • 2014-01-23
    • 2012-06-10
    • 2012-08-09
    • 1970-01-01
    相关资源
    最近更新 更多