我可以在终结器方法中使用哪些对象?

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

我是否错过了关于终结器和字符串的一些观点?

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) { ... } } } 

是的,您肯定可以在终结器和许多其他对象类型中使用字符串。

对于所有这些的权威来源,我会选择通过C#,第3版 ,由Jeffrey Richter编写的CLR一书。 在第21章中,对此进行了详细描述。

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

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

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

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

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

然后,下次运行垃圾收集时,它会再次找到相同的对象,但这次终结器不再需要运行,它已经被执行,因此正常收集对象。

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

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

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

您的程序保留对A的引用,并且仅保留A.

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

此时GC将找到所有这些对象,但是后来注意到B有一个终结器,它还没有运行。 因此,GC会将B放在可自由列表上,并递归地将C,D,E等从GC列表中取出到Z,因为由于B突然变得不合格 ,所以其余部分也是如此。 请注意,其中一些对象也放置在可自由列表本身,因为它们自己有终结器,但它们引用的所有对象都将在GC中存活。

然而,收集了A.

让我清楚地说明上一段。 此时,A已被收集,但是B,C,D等直到Z 仍然活着,好像什么也没发生过一样 。 虽然您的代码不再具有对其中任何一个的引用,但是可释放列表具有。

然后,终结器线程运行,并最终确定可释放列表中的所有对象,并列表中取出对象。

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

所以这肯定有效,那么什么是大布鲁哈呢?

问题在于终结器线程。 该线程不假设它应该最终确定这些对象的顺序。 它没有这样做,因为在许多情况下它不可能这样做。

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

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

换句话说,你不能做的事情总结如下:

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

那么, 回到你的问题

问:我可以在终结器方法中使用字符串吗?
答:是的,因为字符串没有实现终结器,并且不依赖于具有终结器的其他对象,因此在终结器运行时会活着并且踢。

使你采取错误路径的假设是qustion的第二句话:

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

正确的句子是:

在终结器内部,我不能使用具有终结器的其他对象,因为它们可能已经完成。


对于某个例子,终结器无法知道正确完成两个对象的顺序,请考虑两个相互引用的对象,并且两个对象都有终结器。 终结器线程必须分析代码以确定它们通常将被处理的顺序,这可能是两个对象之间的“舞蹈”。 终结器线程不执行此操作,它只是在一个接一个之前完成,并且您无法保证哪个是第一个。


那么,有没有时间从我自己的终结器访问也有终结器的对象是否安全

唯一有保障的安全方案是您的程序/类库/源代码拥有这两个对象,以便您知道它是。

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

例:

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

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

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

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

所以问题是,我是否需要将该文件名的副本复制到manager对象中,以避免在最终确定期间出现问题?

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

但是,在这种情况下,如果您知道缓存对象的终结器不存在,或者未触及该文件,则管理器可以读取缓存对象的filename属性,并删除该文件。

但是,既然你现在有一个非常奇怪的依赖,我肯定会反对它。

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

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

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

有关正确使用Dispose和Fianlizers的绝佳参考,请阅读IDisposable接口的正确使用方法