在我的终结器中处理System.IDisposable对象的释放

4
在StackOverflow上有很多关于如果我的对象管理其他实现了System.IDisposable的托管对象应该怎么做的讨论。
注意:以下我不是在谈论非托管代码。我完全理解清除非托管代码的重要性。
大多数讨论都说,如果你的对象拥有另一个实现了System.IDisposable的托管对象,则你也应该实现System.IDisposable,在这种情况下,你应该调用你的对象持有的可处理对象的Dispose()。这是合乎逻辑的,因为你不知道你所拥有的可处理对象是否使用了非托管代码。你只知道另一个对象的创建者认为,当你不再需要该对象时,调用Dispose会很明智。
在StackOverflow上,这里提供了一个非常好的可处理模式的解释,由社区维基编辑: IDisposable接口的正确使用 通常情况下,在我读到的链接中也经常出现以下内容:
“你不知道两个对象被销毁的顺序。在你的Dispose()代码中,你试图摆脱的托管对象可能已经不存在了。”
这让我感到困惑,因为我认为只要任何对象持有对X对象的引用,那么对象X就不会也不能被终结。
换句话说:只要我的对象持有对X对象的引用,我可以确定对象X没有被终结。
如果这是真的,那么为什么如果我一直持有对我的对象的引用直到我的终结,我所引用的对象已经被终结了呢?

1
这是我的理解:如果Y是对X唯一的引用,但Y变成孤儿了,那么YX都将被销毁。由于顺序不能保证,所以X可能会在Y之前被销毁。 - Pieter Witvoet
1
常常情况比你想象的更加复杂。我指的是Eric Lippert博客上的一篇文章,“当你所知道的一切都是错的”。http://ericlippert.com/2015/05/18/when-everything-you-know-is-wrong-part-one/ - Jodrell
5个回答

5
实际情况介于两者之间:
  • 该对象无法进行垃圾回收,因此对象不再“存在”的可能性是不存在的
  • 当没有其他非可终止对象引用它时,对象可以被终止。

如果X对象引用Y对象,但两者均为可终止对象,则完全有可能会在X对象之前终止Y对象,甚至可能同时终止它们。

如果您的假设是正确的,那么您可以创建两个相互引用的对象(并具有终结器),它们永远不能进行垃圾回收,因为它们永远无法完成终止处理。


你最后的评论解释了一切。显然,即使在我的 finalize 开始时,我也不确定我创建的对象是否仍然存在。这意味着我不能在我的 finalize 中做太多事情,只能确保我的非托管对象被处理。显然,在我的 Dispose(false) 中,我不应该使用任何我的对象,即使是我自己创建的,并且没有分享给其他人。 - Harald Coppoolse
@HaraldDutch 如果你正在编写终结器代码,这是不必要的,除非你有未管理的资源,那么你应该采取“一切不可预测”的态度。 - Jodrell
@HaraldDutch:这取决于你所说的“使用”的含义。你可以假设它们存在,但不能假设它们已经最终确定了。 - Jon Skeet
我指的是“使用”,即调用它的任何方法、属性或字段。 - Harald Coppoolse
1
@HaraldDutch:如果你调用一个即使在finalization之后仍然有效的方法,那么就没问题。但是如果你调用一个依赖于对象未被finalized的方法,那么可能会出现问题。 - Jon Skeet

1

引用Eric Lippert的话,当你所知道的一切都是错误的,第二部分

Myth: Keeping a reference to an object in a variable prevents the finalizer from running while the variable is alive; a local variable is always alive at least until control leaves the block in which the local was declared.

{   
     Foo foo = new Foo();
     Blah(foo);  // Last read of foo
     Bar();
     // We require that foo not be finalized before Bar();
     // Since foo is in scope until the end of the block,
     // it will not be finalized until this point, right?
}

The C# specification states that the runtime is permitted broad latitude to detect when storage containing a reference is never going to be accessed again, and to stop treating that storage as a root of the garbage collector. For example, suppose we have a local variable foo and a reference is written into it at the top of the block. If the jitter knows that a particular read is the last read of that variable, the variable can legally be removed from the set of GC roots immediately; it doesn’t have to wait until control leaves the scope of the variable. If that variable contained the last reference then the GC can detect that the object is unreachable and put it on the finalizer queue immediately. Use GC.KeepAlive to avoid this.

Why does the jitter have this latitude? Suppose the local variable is enregistered into the register needed to pass the value to Blah(). If foo is in a register that Bar() needs to use, there’s no point in saving the value of the never-to-be-read-again foo on the stack before Bar() is called. (If the actual details of the code generated by the jitter is of interest to you, see Raymond Chen’s deeper analysis of this issue.)

Extra bonus fun: the runtime uses less aggressive code generation and less aggressive garbage collection when running the program in the debugger, because it is a bad debugging experience to have objects that you are debugging suddenly disappear even though the variable referring to the object is in scope. That means that if you have a bug where an object is being finalized too early, you probably cannot reproduce that bug in the debugger!

See the last point in this article for an even more horrid version of this problem.


越来越多的时候,我得出结论:如果我的类拥有实现System.IDisposable的对象,那么我也应该实现System.IDisposable,并确保在我的Dispose期间调用它们的Dispose而不是在我的Finalize期间调用。从我的终结器开始,我无法确定我拥有的托管对象是否已经被终结,因此在终结时,我只能访问非托管对象,最好通过调用Dispose(false)来完成。 - Harald Coppoolse
@HaraldDutch 是正确的。除非您有未经管理的资源,否则应避免使用终结器。如果您具有 IDisposable 类型的成员变量,则您的类应实现 IDisposable - Jodrell

0
在大多数情况下,当对一个持有对一个或多个IDisposable对象的引用的对象调用Finalize时,以下一种或多种情况将适用:
  1. 其他对象已经被清理,在这种情况下,调用Dispose最好是无意义的。
  2. 其他对象已经被调度为尽快完成清理工作,但还没有执行,在这种情况下,调用Dispose可能是不必要的。
  3. 其他对象的清理代码不能在终结器线程上下文中安全使用,在这种情况下,调用Dispose很可能会造成灾难性后果。
  4. 其他对象仍然被其他代码使用,在这种情况下,调用Dispose很可能会造成灾难性后果。

在一些情况下,代码了解与之交互的IDisposable对象,以知道上述任何情形都不适用或触发清理操作,但是,这些情况可能更适合于让其他对象提供一个方法而不是Dispose,让正在被终止的对象调用它。


0
如果正确地执行,您就不必担心处理已经被处理的对象。每个Dispose的实现应该在之前已经被处理过的情况下什么都不做。
因此,确实无法知道任何子对象是否已经被处理或已经完成(因为最终完成的顺序是随机的,请参见其他帖子),但您仍然可以安全地调用它们的Dispose方法。

您写道:“Dispose 的每个实现应该在已经被处理后不做任何事情” 。问题不是如果我创建的对象已被处理,是否应再次处置它:当然不是。问题是:我的对象创建了一个对象,没有其他人知道它。我的对象还没有对其进行处理,因为没有人调用其处理程序。在我的对象完成时,它是否应调用其所创建的对象的处理程序?我的对象仍然引用它,因此另一个对象尚未完成,我是正确的吗? - Harald Coppoolse
从其他答案中我了解到,在我的 finalize 开始时,我不确定是否仍然存在任何托管对象 - 除了我在 finalize 中构造的对象。我唯一可以操作的对象是那些我没有自己处理的非托管对象。 - Harald Coppoolse

0
在得到所有答案后,我创建了一个小程序展示了Jodrell所写的内容(感谢Jodrell!)
  • 只要对象不再使用,即使我有对它的引用,它也可以被垃圾回收
  • 仅在非调试状态下才会执行此操作。

我编写了一个简单的类来分配非托管内存和MemoryStream。后者实现了System.IDisposable接口。

根据StackOverflow上的每个人的建议,如果我的Dispose方法被调用,我应该实现System.IDisposable并释放非托管内存以及Dispose托管的memoryStream,但是如果我的finalizer被调用,我只需要释放非托管内存。

我编写了一些诊断控制台消息。

class ClassA : System.IDisposable
{
    IntPtr memPtr = Marshal.AllocHGlobal(1024);
    Stream memStream = new MemoryStream(1024);
        
    public ClassA()
    {
        Console.WriteLine("Construct Class A");
    }
    
    ~ClassA()
    {
        Console.WriteLine("Finalize Class A");
        this.Dispose(false);
    }
    
    public void Dispose()
    {
        Console.WriteLine("Dispose()");
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }
    
    public void Dispose(bool disposing)
    {
        Console.WriteLine("Dispose({0})", disposing.ToString());
        if (!this.IsDisposed)
        {
            if (disposing)
            {
                Console.WriteLine("Dispose managed objects");
                memStream.Dispose();
            }

            Console.WriteLine("Dispose unmanaged objects");
            Marshal.FreeHGlobal(memPtr);                
        }
    }

    public bool IsDisposed { get { return this.memPtr == null; } }
}

这个程序遵循了Dispose模式,如多次描述的那样,例如在stackoverflow中的Proper use of the IDisposable interface

顺便说一下:为了简单起见,我省略了异常处理。

一个简单的控制台程序创建对象,不使用它,但保留对它的引用并强制垃圾收集器进行回收:

private static void TestFinalize()
{
    ClassA a = new ClassA() { X = 4 };

    Console.WriteLine("Start Garbage Collector");
    GC.Collect();
    GC.WaitForPendingFinalizers();
    Console.WriteLine("Done");
}

请注意,变量a在过程结束之前一直持有对对象的引用。我忘记了Dispose,所以我的终结器应该负责处理Dispose。

从主函数调用此方法。从调试器运行(发布)构建并通过命令提示符运行它。

  • 如果从调试器运行,则对象保持活动状态直到过程结束,因此直到垃圾收集器完成收集为止
  • 如果从命令提示符运行,则对象在过程结束之前被终结,即使我仍然拥有对该对象的引用。

所以Jodrell是正确的:

  • 非托管代码需要Dispose()和Finalize,使用Dispose(bool)

  • 托管可释放对象需要Dispose(),最好通过Dispose(bool)进行。在Dispose(bool)中,只有在disposing时才调用托管对象的Dispose()

  • 不要相信调试器:它会使对象在不同的时间终结而没有调试器


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