一个近乎完美的Finalize配合Dispose的设计模板

  在.NET中,由于有了垃圾回收机制的支持,对象的析构和以前C++有了很大的不同,这就要求程序员在设计类型的时候,充分理解.NET的机制,明确怎样利用Dispose方法和Finalize方法来保证一个对象正确而高效地被析构。

  我们知道,在.NET环境中,托管的资源都将由.NET的垃圾回收机制来释放,而一些非托管资源则需要程序员手动地进行将他们释放。.NET提供了主动和被动两种释放非托管资源的方式,即IDisposable接口的Dispose方法类型自己的Finalize方法。

  1.using的实现原理

  在C#中,using语句提供了一个高效的调用对象Dispose方法的方式。对于任何IDispose接口的类型,都可以使用using语句,而对于那些没有实现IDisposable接口的类型,使用using语句会导致一个编译错误。

  先来看下using语句的基本语法: 

using (MemoryStream ms=new MemoryStream())
{
   //...
}

   在上面的代码中,using语句的一开始定义了一个MemoryStream的对象,之后再整个语句块中都可以使用ms,在using语句块结束的时候,ms的Dispose方法将会被自动调用。using语句不仅免除了程序员输入Dispose调用的代码,它还提供了机制保证Dispose方法被调用,无论using语句块顺利执行结束,还是抛出一个异常。事实上,C#编译器为using语句自动添加了try/finally块,所以Dispose方法能够保证被调用到,下面的两端代码经过编译后内容将完全一样:

using(MyDispose md=new MyDispose())
{
   md.DoWork();
}

等价于:

MyDispose md;
try
{
   md=new MyDispose();
   md.DoWork();
}
finally
{
  md.Dispose();
}

  在了解了using的实现原理之后,DebugLZQ提醒:要避免在使用using时时常犯的错误,那就是千万不要试图在using语句块外初始化对象,如下面代码所做的:

MyDispose md=new MyDispose();
using(md)
{
  md.DoWork();
}

  看上去似乎没有任何问题,但是在多线程的程序中,上述代码就会有隐患。试想当md被初始化后程序突然产生一个异常而中断,那md对象中的非托管资源就没有机会得到释放,而这对系统来说危害是非常大的。所以在任何时候都应该在using语句块中初始化需要使用的对象。

  关于using详细,请参考DebugLZQ博文:"using" in C#

   2.Dispose调用时机

   上面DebugLZQ介绍了Dispose方法,正是因为垃圾回收机制掩盖了对象内存回收的时间,考虑到很多情况下程序员仍然希望在对象不再被使用时进行一些清理工作,所以.NET提供了IDisposable接口并且在其中定义了Dispose方法。

  但是,我们要注意实现了Dispose方法不能得到任何有关释放的保证,Dispose方法的调用依赖于类型的使用者(就是我们程序员),当类型被不恰当的使用时,Dispose方法将不会被调用。但是,using等语法的存在还是帮助了Dispose方法被调用。

  废话不多说,就这个就讲这么多~

  3.Finalize方法的机制

  如1、2所说,由于Dispose方法的调用依赖于使用者,为了弥补这一缺陷,.NET同时提供了Finalize方法Finalize方法通常被具有C++开发经验的程序员称为析构方法,但它的执行方法却和传统C++中的析构函数完全不同。Finalize方法在GC执行垃圾回收时调用,具体的机制是这样的:

  • 当每个包含Finalize方法的类型的实例对象被分配时,.NET会在一张特定的表中添加一个引用并且指向这个实例对象。方便起见起见就称该表为“带析构对象表”。
  • 当GC执行并且检测到一个不被调用的对象时,需要进一步检查“带析构对象表”来查看该对象类型是否具有Finalize方法,如果没有则该对象被视为垃圾,如果存在Finalize方法,则把该对象的引用从“带析构对象表”移动到另外一张表中,这里暂时称它为“等待析构表”。并且该对象实例被视为仍然在被使用。
  • CLR酱油一个单独的线程负责处理“等待析构表”,其方法就是一次通过引用调用其中每个对象的Finalize方法,然后删除引用,这是托管堆(DebugLZQ特别提醒,注意:.NET中的垃圾回收,就是释放这个堆上不再被使用的内存对象(因为引用类型的对象存放在这个堆上)。注意区分堆栈,堆栈存放的是值类型对象和托管堆上引用类型对象的指针。)中的对象实例被认为处于不在被使用的状态。
  • 在下一个GC执行时,将释放已经被调用Finalize方法的那些对象实例。

  警告:Finalize方法应该只致力于快速而简单地释放非托管资源,并且尽可能快地返回。不正确的Finalize方法可能包含这样的代码:

  • 没有保护地写文件日志
  • 访问数据库
  • 访问网络
  • 当前对象被付给某个存活的引用

  当Finalize方法试图访问文件系统、数据库系统或者网络时,将会有资源争用和等待的潜在危险。试想一个不断尝试访问离线数据库的Finalize方法,将会在长时间内不会反回,这不仅影响了对象本省的释放,也使得排在Finalize方法队列中的所有后续对象得不到释放,这个连锁反应将很快造成内存耗尽(结果是死机,最近DebugLZQ在医生站项目中就曾遇到了这个情况!!!)。

  而另外一种危险的代码是在Finalize方法中把对象自身又赋值给另外一个存活的引用,这时对象内的一部分资源已经被释放了,而另外一部分则没有,这样一个对象再被激活后,将导致不可预知的后果。

   4.正确地使用Dispose和Finalize方法

  一步一步,写的越来越深,不知道你有没有看懂,但DebugLZQ还得继续,因为问题没有说清楚~

  从安全性上来讲,Finalize方法确实比Dispose方法更加安全,因为它由CLR保证调用。

  但是性能方面,Finalize方法却要差得多!!!

  这一点DebugLZQ简单做一个解释吧:和.NET 垃圾回收机制中3代机制有关,根据.NET的垃圾回收机制,0代,1代,2地的初始分配空间分别为256KB,2MB和10MB。当某个实例在GC执行时被发现仍然在使用,它将被移动到下一代上。并且并不是每次垃圾回收都会回收3代的所有对象,越小的代拥有者越大的回收机会。CLR的基本算法是:执行N代的0代垃圾回收,才会执行一次1代的垃圾回收;执行N次的1代的垃圾回收,才会执行一次2代的垃圾回收。相对于0代的快速释放,1代和2代的对象具有较少的回收机会,结合3中DebugLZQ讲述的Finalize机制,你应该能想明白,为什么Finalize会大幅度地影响性能了。

  5.一个近乎完美的Finalize配合Dispose的设计模板

   DebugZLQ写到这里,各位博友应该知道,正确的类型设计是把Finalize方法作为Dispose方法的后备,只有在使用者没有调用Dispose方法的情况下,Finalize方法才被视为需要执行。下面给出一个正确高效的设计模板,DebugLZQ建议各位博友牢记这个模板并且套用到每一个需要Dispose和Finalize方法的类型上去。

using System;
using System.Collections.Generic;
using System.Text;

namespace PerfectFinalizeDispose
{
    //DebugLZQ
    //http://www.cnblogs.com/DebugLZQ
    public class FinalizeDisposeBase : IDisposable
    {
        // 标记对象是否已被释放
        private bool _disposed = false;

        //  Finalize方法:
        ~FinalizeDisposeBase()
        {
            Dispose(false);
        }

        // 这里实现了IDispose中的 Dispose方法
        public void Dispose()
        {
            Dispose(true);

            //告诉GC此对象的Finalize方法不再需要调用
            GC.SuppressFinalize(this);
        }

        //在这里做实际的析构工作
        //申明为虚方法以供子类在有必要时重写
        protected virtual void Dispose(bool isDisposing)
        {
            // 当对象已经被析构时,不再执行
            if (_disposed)
                return;
            if (isDisposing)
            {
                //在这里释放托管资源
                //只在用户调用Dispose方法时执行
            }
            //在这里释放非托管资源

            //标记对象已被释放
            _disposed = true;
        }
    }

    public sealed class FinalizeDispose : FinalizeDisposeBase
    {
        private bool _mydisposed = false;

        protected override void Dispose(bool isDisposing)
        {
            // Don't dispose more than once.
            if (_mydisposed)
                return;
            if (isDisposing)
            {
                //在这里释放托管的并且在这个类型中申明的资源
            }
            //在这里释放非托管的并且在这个类型中申明的资源

            //调用父类的Dispose方法来释放父类中的资源
            base.Dispose(isDisposing);

            // 设置子类的标记
            _mydisposed = true;
        }

        static void Main()
        {
        }
    }
}

  以上为一个近乎完美的Finalize配合Dispose的设计模板,其中有几点需要各位博友注意:

  • 真正做释放工作的只是virtual的受保护的Dispose方法,事实上这个方法的名字并不重要,仅仅是为了通用和更好的理解,所以命名它为Dispose。
  • 虚方法Dispose需要接受一个bool型的参数,主要用于区分调用方式类型的使用者还是.NET的垃圾回收。前者通过IDisposable的Dispose方法,而后者通过Finalize方法。两者的区别是通过Finalize方法释放资源时不再释放或使用对象中的托管资源,这是因为这时的对象已经处于不被使用的状态,很有可能其中的托管资源已经释放掉了。
  • 在IDisposable的Dispose方法的实现中通过GC.SupressFinalize()方法来告诉.NET此对象在被回收时不需要调用Finalize方法,这一句是改善性能的关键,记住实现Dispose方法的本质目的就是避免所有释放工作都在Finalize方法中进行。
  • 子类型必须定义自己的释放标记来标明子类中的资源是否已经被释放,同时子类的虚方法Dispose方法也只需要释放自己新定义的资源。
  • 确保在虚方法Dispose中做的都是释放工作,有些逻辑上的结束工作需要反复斟酌的,以防止一个简单的赋值语句使对象再度存活。

  6.结束语

  最近工作很忙,时间变得越来越宝贵,拿出时间来胡乱侃侃这个浅谈那个,说实话挺不容易,写作初衷是犹自对.NET的热爱,目的是给各位博友分享一种对.NET的认识、理解,所以转载请注明出自博客园DebugLZQ。

  当然人非圣贤,博文中可能存在一些说法欠妥的地方,欢迎批评指正~

  最后,请点击下面的绿色通道,关注DebugLZQ,共同交流、共同进步~ 

 

posted @ 2012-08-28 20:11  DebugLZQ  阅读(3673)  评论(16编辑  收藏  举报