使用析构函数的IDisposable:需要线程安全的实现吗?

4

这基本上只是为了确保我没有错:

我们有一个实现IDisposal模式的大型资源类。根据设计,它应该以一种允许它被多次调用的方式实现(即使我们当然只尝试调用一次)。我们还实现了一个终结器,该终结器也调用Dispose()方法-作为备份。如果手动调用Dispose(),则Dispose()也将调用GC.SuppressFinalize(this)。

有几个处置模式的示例。其中大多数在处理代码的末尾调用GC.SuppressFinalize(this)。有些人声称,在任何清理之前调用它会更好。后者认为,这样可以确保GC在我们仍在清理时不会并发地调用终结器。

问题:
看起来,在开头放置GC.SuppressFinalize并没有做得更好?我们仍然有竞争条件,对吗?那么是否真的应该使用线程安全的方式实现Dispose()呢?


“Dispose模式”是.NET框架设计准则中描述的一种模式,其中在结尾处调用了GC.SuppressFinalize。这个准则在微软内部进行了广泛讨论,最终形成了这样的模式。 - Steven
3个回答

4
GC只清理那些不可达的对象。
正在执行代码的类仍然是“可达”的,因为它的“this”指针在堆栈上。因此,在执行dispose时,finalizer不会被调用。
所以无论您是在开始还是结束时调用SuppressFinalize都没有关系。
正如下面的评论者所指出的,CLR实现似乎不能保证当实例方法执行时您的对象不会被垃圾回收/终结。唯一可能的“可靠”引用使对象保持活动状态是用于调用对象上的方法的引用,但我对JIT内部知道得不够多,无法对其行为做出确定性的声明,而且它的行为可能会改变。
我将答案保留在这里以便讨论。

1
这并不完全准确,执行实例方法并不能阻止终结。Jitter为this提供生命周期信息,就像为任何本地变量或方法参数一样。但是,确实有一个保证,即没有留下仍然触及对象的代码可以运行。 - Hans Passant
@jdv-Jan de Vaan:这在大多数情况下可能是正确的。然而,至少存在一个例外:http://blogs.msdn.com/b/ricom/archive/2004/05/19/135332.aspx(请在该网站上继续阅读讨论)- 因此,我认为我们应该至少记住并为那些(罕见的)情况做好准备。 - user492238
@Hans Passant:“有一个保证,没有留下任何仍然触及该对象的代码。” - 除了最终化代码,我想是吧? - user492238
如果您参考讨论调用Dispose的终结器部分,我认为这不是良好的行为。请参阅:http://msdn.microsoft.com/en-us/library/b1yfkh5e(v=vs.71).aspx:“(Finalize方法不应引用任何其他对象。)” - user180326
@jdv-Jan de Vaan:不,我的意思是显示最终器可以在类构造函数中的代码执行之间被调用的部分。 - user492238
显示剩余2条评论

3
虽然在看似仍有引用的情况下对象可能会被终结,但这只有在不再有任何东西会引用该对象时才会发生。GC.SuppressFinalize(this)主动引用了当前对象“this”,从而保证它在GC.SuppressFinalize执行之前不会被终结。此外,对象引用存在以处理对象,并可供Dispose方法使用,保证了除非对象已死且某个终结器(无论是其自己的终结器还是其他对象的终结器)使其复活,否则终结器在Dispose开始运行之前不会排队。
由于有些情况下对象可能会被计划进行终结并在不知情的情况下复活,因此保护处理和终结免受冗余操作可能并不是一个坏主意。然而,Microsoft的模式并不好。可终结的对象不应持有对不需要进行终结的任何对象的引用。如果一个对象将持有托管和非托管资源的混合,那么非托管资源应该移入它们自己的类中(有效地将它们转换为托管资源),这样主要对象将仅持有托管资源。

1
Dispose 不应该抛出任何异常。
我会确保 Dispose 中的所有代码都是线程安全的,这样如果它被调用,就不会做什么奇怪的事情。通常,添加检查变量是否为空已经足够解决问题了。
在 Microsoft 的示例中,我只看到在 Dispose 函数的末尾加入了 GC.SuppressFinalize。

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