关于 C# Dispose Pattern 的具体问题

Posted

技术标签:

【中文标题】关于 C# Dispose Pattern 的具体问题【英文标题】:Specific questions about C# Dispose Pattern 【发布时间】:2011-04-17 19:52:44 【问题描述】:

我有几个关于 C# 中的 Dispose 模式的基本问题。

在下面的代码 sn-p 中,这似乎是实现 dispose 模式的标准方式,您会注意到如果 disposing 为 false,则不会处理托管资源。如何/何时处理它们? GC 是否会出现并稍后处理托管资源?但如果是这样,GG.SuppressFinalize(this) 调用会做什么?有人可以给我一个处置托管资源的例子吗?解开事件浮现在脑海中。还要别的吗?模式的编写方式,如果你在“if(disposing)”部分什么都不做,它们似乎会被(稍后)处理掉。评论?

protected virtual void Dispose(bool disposing)
  
    if (!disposed)
    
        if (disposing)
        
            // Dispose managed resources.
        

        // There are no unmanaged resources to release, but
        // if we add them, they need to be released here.
    
    disposed = true;

    // If it is available, make the call to the
    // base class's Dispose(Boolean) method
    base.Dispose(disposing);

// implements IDisposable
public void Dispose()

    Dispose(true);
    GC.SuppressFinalize(this);

我在这个线程How do I implement the dispose pattern in c# when wrapping an Interop COM Object? 中读到的关于 Dispose(bool) 中的锁的内容是真的吗?它说,“元元注释——除此之外,在非托管清理期间永远不要获取锁或使用锁,这一点很重要。”这是为什么?它也适用于非托管资源吗?

最后,是否曾经在不实现 IDisposable 的情况下实现终结器(C# 中的 ~MyClass())?我相信我在某处读到,如果没有非托管资源,终结器和 IDisposable 就不是必需的(或可取的)。但是,我确实看到在某些示例中使用了没有 IDisposable 的终结器(请参阅:http://www.codeproject.com/KB/cs/idisposable.aspx 作为一个示例) 谢谢, 戴夫

【问题讨论】:

感谢大家的所有好评!不幸的是,我只能将一个标记为答案。 【参考方案1】:

这种实现IDisposable 模式的方式是一种故障安全方式:如果客户端忘记调用Dispose,运行时调用的终结器将在稍后调用Dispose(false)(注意这部分在你的样本)。

在后一种情况下,即当终结器调用Dispose 时,托管资源将已被清理,否则相关对象将不符合垃圾回收条件。

但如果是这样,GC.SuppressFinalize(this) 调用会做什么?

运行终结器需要额外的费用。因此,应尽可能避免。调用 GC.SuppressFinalize(this) 将跳过运行终结器,因此可以更有效地对对象进行垃圾回收。

一般来说,应该避免依赖终结器,因为无法保证终结器会运行。 Raymond Chen 在以下帖子中描述了终结器的一些问题:

When do I need to use GC.KeepAlive?

【讨论】:

一个小问题:“在后一种情况下......托管资源已经被清理了”。情况不一定如此:GC 是不确定的,所以也许它们会被清理掉,也许不会。无论哪种方式,您都应该就好像它们已被清理(即,您不应该尝试对它们做任何事情)。【参考方案2】:

没有人回答最后两个问题(顺便说一句:每个线程只问一个)。在 Dispose() 中使用锁对终结器线程非常致命。锁的持有时间没有上限,当 CLR 注意到终结器线程被卡住时,您的程序将在两秒钟后崩溃。而且,这只是一个错误。当另一个线程可能仍然具有对该对象的引用时,您永远不应该调用 Dispose()。

是的,在不实现 IDisposable 的情况下实现终结器并非闻所未闻。任何 COM 对象包装器 (RCW) 都可以做到这一点。 Thread 类也是如此。这样做是因为调用 Dispose() 是不切实际的。在 COM 包装器的情况下,因为不可能跟踪所有引用计数。在 Thread 的情况下,因为必须 Join() 线程以便您可以调用 Dispose() 会破坏拥有线程的目的。

关注 Jon Hanna 的帖子。 99.99% 的情况下,实现自己的终结器确实是错误的。你有 SafeHandle 类来包装非托管资源。你需要一些非常模糊的东西才能被他们包裹起来。

【讨论】:

托管线程在垃圾收集期间被挂起,但我不认为它们在完成期间被挂起。终结的工作方式、启用终结的对象以及它们直接或间接引用的任何对象都不会被垃圾收集,但如果它们符合垃圾收集的条件,它们将在当前的垃圾收集后被终结通行证完成。一旦终结器运行,除非对象重新注册以进行终结,否则它们将不再有资格进行终结,并将在下一次 gc 中被扫除。 @super - 你是对的,不知道我为什么写这个。损坏已消除,谢谢。 @Hans:我认为 Thread 类使用终结器来清理托管线程 ID 的分配。存在的每个单独创建的 Thread 对象都必须有一个不同的 ID;因为线程 ID 只有 32 位,所以框架必须能够重用它们(否则任何在其生命周期内创建了 20 亿线程对象的程序都会死掉)。由于如果两个 Thread 对象具有相同的 ManagedThreadID,则可能会发生坏事,因此使用终结器来确保仅当不再存在对持有它们的 Thread 对象的引用时,ID 才可用于重用。 @super - 不,线程是 CLR 中的核心非托管对象。查看 SSCLI20 源代码的 clr/src/vm/threads.h 中的 IdDispenser 类以进行实际实现。 @Hans:如果创建了一个 Thread 对象但从未启动,究竟会创建什么?我的印象是“真正的”线程在线程启动之前不会被创建,因为在那之前所需的处理器关联和单元状态是未知的。即使对于从未启动的 Thread 对象,是否还会创建其他“非托管对象”?【参考方案3】:

上述模式是一个雄辩地处理处置和最终确定的重叠问题的问题。

在处理时,我们希望:

    处理所有一次性成员对象。 处置基础对象。 释放非托管资源。

最终确定时,我们希望:

    释放非托管资源。

除此之外还有以下问题:

    多次调用处理应该是安全的。调用x.Dispose();x.Dispose(); 应该不会出错 最终化增加了垃圾收集的负担。如果我们可以尽可能避免它,特别是如果我们已经释放了非托管资源,我们希望抑制终结,因为它不再需要。 访问已完成的对象令人担忧。如果一个对象正在被终结,那么任何可终结的成员(也将处理与我们的类相同的问题)可能已经也可能没有被终结,并且肯定会在终结队列中。由于这些对象也可能是托管的一次性对象,并且由于处置它们会释放其非托管资源,因此我们不希望在这种情况下处置它们。

您提供的代码将(一旦您添加调用 Dispose(false) 的终结器管理这些问题。在调用 Dispose() 的情况下,它将清理托管和非托管成员并抑制终结,同时也防止多次调用(但在这方面它不是线程安全的)。在调用终结器的情况下,它将清理非托管成员。

但是,只有在同一类中组合托管和非托管关注点的反模式才需要此模式。更好的方法是通过一个只与该资源相关的类来处理所有非托管资源,无论是 SafeHandle 还是您自己的单独类。然后您将拥有两种模式之一,其中后者很少见:

public class HasManagedMembers : IDisposable

   /* more stuff here */
   public void Dispose()
   
      //if really necessary, block multiple calls by storing a boolean, but generally this won't be needed.
      someMember.Dispose(); /*etc.*/
   

这没有终结器,也不需要终结器。

public class HasUnmanagedResource : IDisposable

  IntPtr _someRawHandle;
  /* real code using _someRawHandle*/
  private void CleanUp()
  
     /* code to clean up the handle */
  
  public void Dispose()
  
     CleanUp();
     GC.SuppressFinalize(this);
  
  ~HasUnmanagedResource()
  
     CleanUp();
  

这个版本很少见(甚至在大多数项目中都不会发生)只有处理唯一的非托管资源的处置处理,类是一个包装器,如果处置没有,终结者也会做同样的事情'不会发生。

由于 SafeHandle 允许为您处理第二种模式,因此您根本不需要它。无论如何,我给出的第一个示例将处理您需要实现IDisposable 的绝大多数情况。您的示例中给出的模式只能用于向后兼容,例如当您从使用它的类派生时。

【讨论】:

【参考方案4】:

...您会注意到,如果 disposing 为 false,则不会处理托管资源。如何/何时处理它们?

您没有在示例中包含它,但该类型通常会有一个析构函数,它会调用Dispose(false)。因此,当disposingfalse 时,您“知道”您正在调用终结器,因此应该 *不*访问任何托管资源,因为他们可能已经已经完成了。

GC 终结过程仅确保调用终结器,而不是调用它们的顺序

GG.SuppressFinalize(this) 调用有什么作用?

它会阻止 GC 将您的对象添加到终结队列并最终调用 object.Finalize()(即您的析构函数)。这是性能优化,仅此而已。

按照模式的编写方式,如果您在“if (disposing)”部分什么都不做,它们似乎会被(稍后)处理掉

也许;这取决于类型的编写方式。 IDisposable 成语的一个主要观点是“确定性终结”——说“我希望你的资源现在被释放”并让它有意义。如果您“忽略”disposing=true 块并且不“转发”Dispose() 调用,则会发生以下两种情况之一:

    如果该类型具有终结器,则对象的终结器最终可能会在“稍后”的某个时间被调用。 如果该类型没有终结器,则托管资源将“泄漏”,因为永远不会在它们上调用 Dispose()

在非托管清理期间永远不要获取锁或使用锁,这一点很重要。”这是为什么?它也适用于非托管资源吗?

这是一个理智问题——你的理智。您的清理代码越简单越好,并且始终Dispose() 抛出异常是个好主意。使用锁可能会导致异常或死锁,这两种情况都是毁掉你一天的好方法。 :-)

在没有实现 IDisposable 的情况下实现了终结器(C# 中的 ~MyClass())

可以,但会被认为是不好的风格。

【讨论】:

【参考方案5】:

处理对象的正常方法是调用它的Dispose() 方法。这样做后,SuppressFinalize 调用会从终结器队列中移除对象,将其转变为可以轻松进行垃圾回收的常规托管对象。

终结器仅在代码未能正确处置对象时使用。然后终结器调用Dispose(false),以便对象至少可以尝试清理非托管资源。由于对象引用的任何托管对象在此阶段可能已经被垃圾回收,因此对象不应尝试清理它们。

【讨论】:

【参考方案6】:

您可能不想尝试通过模式来了解处置,而是想翻转事物并尝试根据 CLR 基础知识和 IDisposable 接口的预期用途来了解为什么以这种方式实现模式。在http://msdn.microsoft.com/en-us/magazine/cc163392.aspx 有一个很好的介绍,应该可以回答你所有的问题(以及一些你没想到的问题)。

【讨论】:

【参考方案7】:

如果您的类将直接持有非托管资源,或者如果它可能会被这样做的后代类继承,Microsoft 的处置模式将提供一种将终结器和处置器联系在一起的好方法。如果您的类或其后代不存在直接持有非托管资源的现实可能性,则应删除模板代码并直接实现 Dispose。鉴于 Microsoft 强烈建议将非托管资源包装在其唯一目的是保存它们的类中 (*)(并且有类似 SafeHandle 之类的类正是为此目的),因此真的不再需要模板代码了。

(*) .net 中的垃圾收集是一个多步骤的过程;首先,系统确定哪些对象没有在任何地方引用;然后它会列出一个在任何地方都没有引用的 Finalize'able 对象列表。该列表及其上的所有对象将被重新声明为“活动的”,这意味着它们引用的所有对象也将是活动的。此时,系统将执行实际的垃圾收集;然后它将运行列表中的所有终结器。如果一个对象成立,例如对字体资源(非托管)的直接句柄以及对其他十个对象的引用,这些对象又持有对一百多个对象的直接或间接引用,然后由于非托管资源,该对象将需要终结器。当对象到期被收集时,它和它持有直接或间接引用的 100 多个对象都没有资格被收集,直到它的终结器运行后通过。

如果对象不是持有对字体资源的直接句柄,而是持有对持有字体资源的对象的引用(仅此而已),则后者对象将需要终结器,但前者不需要(因为它不持有对非托管资源的直接引用。只有一个对象(持有终结器的对象)而不是 100 多个对象必须在第一次垃圾回收中幸存下来。

【讨论】:

以上是关于关于 C# Dispose Pattern 的具体问题的主要内容,如果未能解决你的问题,请参考以下文章

C#内存和dispose相关问题

C# IDisposable 类,正确使用 Dispose,获取错误“dispose”需要 1 个参数

unity-tolua之Dispose释放引用

C#学习笔记---Dispose(),Finalize(),SuppressFinalize

C# Dispose模式

C# Dispose Finalize