重新学.Net[七]——垃圾回收和资源管理[下]
在前面说了GC的工作原理。需要注意的是,GC只能回收托管堆中的资源。其他一些非托管资源,比如文件资源,缓冲区,互斥体之类,无法通过GC自动回收。必须通过开发人员自己编程实现对其的回收(有时候会觉得CLR的资源管理也会比较麻烦,因为它有一部分自动的,有用一部分手动的,但和C++比比,我们应该很知足了^_^)。
很自然的一种编码方式是将回收资源的函数写入终结函数Finalize中,GC启动回收托管资源的时候顺便把非托管资源一并回收了(有时候我会简单得说GC回收了,指的就是在GC启动时正好执行该代码一并回收去了)。但是,这会带来很多的问题。最大的问题是时机问题。开发人员不能决定GC何时启动(包括调用GC.Collect()也不行),这使得无法确定非托管资源何时会被释放,也无法决定不同对象占用的资源的释放顺序。
因此,我们需要按照一种被称为Dispose的模式来进行非托管资源的管理,先看一段比较经典的Dispose模式编码:
public class UnsafeSourceHolder : IDisposable
{
private IntPtr buffer;
private SafeHandler resource;
private bool disposed;
public UnsafeSourceHolder()
{
buffer = ...;
resource = ...;
disposed = false;
}
protected virtual void Dispose(bool disposing)
{
if (disposed) return;
ReleaseBuffer(buffer);
if (disposing)
{
if (resource != null) resource.Dispose();
}
disposed = true;
}
~UnsafeSourceHolder()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public void Close()
{
Dispose();
}
public void DoSomething()
{
if (disposed) throw new ObjectDisposedException("不能使用被释放的资源");
}
}
从上面的代码中,我们来看看Dispose模式的关键之处。首先,如果一个类(上例中的UnsafeSourceHolder )中包含非托管资源(在上例中,buffer和resource均代表非托管资源),它需要实现 IDisposable接口,这个接口中只有一个public void Dispose()方法。当然,如果你不实现该接口(比如,把Dispose()中的内容放到Close()中去),在理论上是没有任何问题的。但,这样做至少有两个不好的地方:
1.你失去了和调用人说话的一种语言(我们默认调用人看到一个类实现了IDisposable接口就认为该类中含有非托管资源),调用人可能不知道这个类中包含非托管资源;
2.你失去了某些方便的语法支持(比如C#中的using会自动帮助你调用该接口的Dispose方法)。
因此这样做是必须的。而为什么还要有一个Close()方法呢,原因也有两个:
1.调用人会比较习惯Close()这样的命名;
2.CLR将Dispose()方法作为一个显性接口方法,你必须这样调用:(IDisposable i).Dispose(),比较麻烦。
恩,所以综合考虑,当一个类中有托管资源的时候,你需要实现IDisposable接口,并暴露上面两个API供调用者使用。
说到了调用,我们再来看看,一个调用该类对象的人,有几种触发资源回收的方式。一种就是显性的调用Dispose或Close方法(或利用语法偷偷调用),通知回收资源。另外一种,是让GC启动自动回收资源。什么?我说过GC无法回收非托管资源,恩,是的,但是我已经重载了(相当于)Finalize()方法。什么?你没看到,那么请注意~UnsafeSourceHolder()这个函数。这可不是C++的析构函数,在上例中写了此函数,相当于写如下代码:
protected override void Finalize()
{
try
{
Dispose();
}
finally
{
base.Finalize();
}
}
之所以按貌似析构函数的方法来写,是因为很多开发人员不会按规则写入上的代码,以至于终结函数抛出异常,导致进程Down掉,或资源泄露。因此,请你按这种让许多C++程序员会感到异常不爽的方式来写(我个人觉得这种设计有点画蛇添足了,一个不会写try-finally这样代码的开发人员,你如何确定它会写析构函数那样的代码??)。
但是但是,这样我们仔细看看代码我们可以发现。这两种调用方式是有所区别的,真实对非托管资源进行处理的是Dispose(bool disposing)函数,Dispose()方法调用Dispose(true),而GC调用的是Dispose(false)。这有什么区别呢?讲清楚这个问题,我们需要将非托管资源分成两类(这些是我自己的说法,可能会有bug哦^_^),一类是只被一个对象享用的非托管资源,在上例中以buffer表示。另一类是被许多对象享用的非托管资源,在上例中以resouce表示。自己独享的资源当此对象销亡时必须释放,所以在GC调用时会考虑释放该资源。但另一类被共享的资源则不能由GC来释放,因为GC释放次序是不定的,无法确认释放这个资源后是否有其他对象在使用该资源。而了解这个的,只有调用者自己。因此只能依赖于调用者自己手动调用Dispose()或Close()来释放该资源。也就是说,手动的调用,会释放该对象使用的所有非托管资源,而自动调用只能释放该对象独享的非托管资源。
但,如果调用者出现错误,在释放了非托管资源后,又再次使用它,该怎么办?这时候,要求对象能够告诉调用者:Are you craze?。在上例中,用disposed变量来记录资源的使用状态,在非法使用时抛出ObjectDisposedException异常(这也是Dispose模式推荐的一部分内容)。
在上面代码中,还有一句GC.SuppressFinalize(this);这时告诉GC不要自动回收该对象。这里的原因我也不是很清楚,有说是已显性回收该对象没必要再自动回收了。我个人觉得不对,因为显性回收的只是非托管资源而已,托管资源并未回收。查了下MSDN和一些书,觉得更好的解释应该是如果Dispose回收时正好执行垃圾回收会产生冲突(因为垃圾回收启动时导致的线程挂起,并不会影响非托管资源的回收,这样会从两个地方回收该资源产生冲突),所以要调用GC.SuppressFinalize使得垃圾回收机制会在一段时间内不再尝试回收该对象。请注意,这是一段时间内而不是永远。
上面就是Dispose模式的一个基本结构,简单总结一下有如下一些要点:
1.实现IDisposable接口。
2.提供Close()函数,内部调用Dispose()函数。
3.实现一个Dispose(bool disposing)函数,处理不同的非托管资源。
4.实现析构函数(加个伪字好了^_^),使其能在GC启动时自动回收。
5.当调用已释放资源时抛出ObjectDisposedException异常(同时要允许多次清除同一资源)。
6.使用GC.SuppressFinalize函数,防止清理冲突。
当然还可以根据情况进一步的改良它的实现(比如在资源回收时加锁),但它的整体思路和基本接口不应该改变。当你实现的类中有设计非托管资源的管理时,请按照该模式来实现。但,当你类中没有非托管资源的时候,请不要没事找事,想一想上中那个需要顿悟的地方哦^_^。
posted on 2007-02-10 21:46 duguguiyu 阅读(1716) 评论(0) 编辑 收藏 举报