重学c#系列——非托管实例(五)

前言

托管资源到是好,有垃圾回收资源可以帮忙,即使需要我们的一些小小的调试来优化,也是让人感到欣慰的。但是非托管资源就显得苍白无力了,需要程序员自己去设计回收,同样有设计的地方也就能体现出程序员的设计水平。

托管类在封装对非托管资源的直接引用或者间接引用时,需要制定专门的规则,确保非托管资源在回收类的一个实例时释放。

为什么要确保呢?

是这样子的,画一个图。

上图中托管中生成并引用非托管,一但非托管和托管中的引用断开(托管资源被回收),那么这个时候非托管资源还在,那么释放这个问题就有一丢丢困难。

常见的有两种机制来自动释放非托管资源。

  1. 声明一个构析函数作为一个类的一个成员。

  2. 在类中实现System.IDisposable.

好的,接下来就开始看例子吧。

正文

构析函数

先从构析函数看起吧。

class Resource
{
	~Resource()
	{
           //释放资源
	}
}

在IL中是这样子的。

protected override void Finalize()
{
	try
	{
           //构析函数写的
	}
	finally
	{
		base.Finalize();
	}
}

简单介绍一下这个Finalize 是一个终结器,我们无法重写,文档中原文是这样子的。

从包装非托管资源的 SafeHandle 派生的类(推荐),或对 Object.Finalize 方法的重写。 SafeHandle 类提供了终结器,因此你无需自行编写。

这个SafeHandle 是啥呢?是安全句柄。这东西学问很大,非该文重点,先可以理解为句柄即可。

这里简单介绍一下句柄。

职业盗图:

再次职业盗图:

假设有一个句柄为0X00000AC6。有一个区域存储这各个对象的地址,0X00000AC6指向这个区域里面的区域A,A只是这个区中的一个。这个A指向真实的对象在内存中的位置。

这时候就有疑问了,那么不是和指针一个样子吗?唯一不同的是指针的指针啊。是的,就是指针的指针。但是为啥要这么做呢?

是这样子的,对象在内存中的位置是变化的,而不是不变的。我们有时候看到电脑下面冒红灯,这时候产生了虚拟内存,实际就是把硬盘当做内存了。但是我们发现电脑有点卡后,但是程序没有崩溃。

当对象内存写入我们的硬盘,使用的时候又读出来了,这时候内存地址是变化了。这时候在内存中的操作是区域A的值变化了,而句柄的值没有变化,因为它指向区域A。

现在我们通过实现构析函数来实现释放非托管资源,那么这种方式怎么样呢?这种方式是存在问题的,所以现在c#的构析函数去释放非托管谈的也不多。

主要问题如下:

  1. 无法确认构析函数何时执行,垃圾回收机制不会马上回收这个对象,那么也就不会立即执行构析函数。

  2. 构析函数的实现会延迟该对象在内存中的存在时间。没有构析函数的对象,会在垃圾回收器中一次处理从内存中删除,实现构析函数的对象需要两次。

然后所有对象的终结器是由一个线程来完成的,如果Finalize中存在复杂的业务操作,那么系统性能下降是可以预见的。

实现IDisposable

看例子:

class Resource : IDisposable
{
	public void Dispose()
	{
	   //释放资源
	}
}

然后只要用完调用Dispose即可。

但是可能有时候程序员忘记主动调用了Dispose。

所以改成这样。

class Resource : IDisposable
{
	bool _isDisposed=false;
	public void Dispose()
	{
		//释放资源
		_isDisposed = true;
		//标志不用掉析构函数
		GC.SuppressFinalize(this);
	}
	~Resource()
	{
		if (_isDisposed)
		{
			return;
		}
		this.Dispose();
	}
}

那么是否这样就结束了呢?

不是的。

文档中这样介绍道:任何非密封类都应具有要实现的附加 Dispose(bool) 重载方法。

为什么这样说呢?因为是这样子的,不是密封类,那么可能会成为某个类的基类,那么子类就要考虑基类如何释放啊,所以加一个重载方法。

注:从终结器调用时,disposing 参数应为 false,从 IDisposable.Dispose 方法调用时应为 true。 换言之,确定情况下调用时为 true,而在不确定情况下调用时为 false。
class Resource : IDisposable
{
	bool _isDisposed=false;
	public void Dispose()
	{
		//释放资源
		Dispose(true);
		//标志不用掉析构函数
		GC.SuppressFinalize(this);
	}
	~Resource()
	{
		this.Dispose(false);
	}
	protected virtual void Dispose(bool disposing)
	{
		if (_isDisposed)
		{
			return;
		}
		if (disposing)
		{
			//释放托管相关资源
		}
		//释放非托管资源
		_isDisposed = true;
	}
}

看下思路:

Dispose(bool) 方法重载

方法的主体包含两个代码块:

释放非托管资源的块。 无论 disposing 参数的值如何,都会执行此块。

释放托管资源的条件块。 如果 disposing 的值为 true,则执行此块。 它释放的托管资源可包括:

实现 IDisposable 的托管对象。 可用于调用其 Dispose 实现(级联释放)的条件块。 如果你已使用 System.Runtime.InteropServices.SafeHandle 的派生类来包装非托管资源,则应在此处调用 SafeHandle.Dispose() 实现。

占用大量内存或使用短缺资源的托管对象。 将大型托管对象引用分配到 null,使它们更有可能无法访问。 相比以非确定性方式回收它们,这样做释放的速度更快。

那么为什么明确去释放实现IDisposable 的托管资源呢?

文档中回答是这样子的:

如果你的类拥有一个字段或属性,并且其类型实现 IDisposable,则包含类本身还应实现 IDisposable。 实例化 IDisposable 实现并将其存储为实例成员的类,也负责清理。 这是为了帮助确保引用的可释放类型可通过 Dispose 方法明确执行清理。

给个完整例子。

class Resource : IDisposable
{
	bool _isDisposed=false;
	private SafeHandle _safeHandle = new SafeFileHandle(IntPtr.Zero, true);
	public void Dispose()
	{
		//释放资源
		Dispose(true);
		//标志不用掉析构函数
		GC.SuppressFinalize(this);
	}
	~Resource()
	{
		this.Dispose(false);
	}
	protected virtual void Dispose(bool disposing)
	{
		if (_isDisposed)
		{
			return;
		}
		if (disposing)
		{
			_safeHandle?.Dispose();
			//释放托管相关资源
		}
		//释放非托管资源
		_isDisposed = true;
	}
}

_safeHandle 和 Resource 一样同样可以通过构析函数去释放非托管,但是呢,如果自己Resource 主动Dispose去释放,那么最好把它的子对象(托管)的Dispose给执行了,好处上面写了。

那么这时候为什么在构析函数中为显示为false呢?因为构析函数这时候本质是在终结器中执行,属于系统那一套,有太多不确定因素了,所以干脆_safeHandle 自己去调用自己析构函数。

后来我发现.net core和.net framework,他们的构析函数执行方式是不一样的。

举个栗子:

static void Main(string[] args)
{
	{
		Resource resource = new Resource();
	}
	GC.Collect();
	Console.Read();
}

在.net framework 中马上回去调用构析函数,但是在.net core中并不会,等了几分钟没有反应。

原因可以在:

https://github.com/dotnet/corefx/issues/5205

知道了大概怎么回事。

好的,回到非托管中来。

那么继承它的子类怎么写呢?

class ResourceChild: Resource
{
	bool _isDisposed = false;
	~ResourceChild()
	{
		Dispose(false);
	}
	protected override void Dispose(bool disposing)
	{
		if (_isDisposed)
		{
			return;
		}
		if (disposing)
		{
			//释放托管相关资源
		}
		//释放非托管资源
		_isDisposed = true;
		base.Dispose();
	}
}

非托管有太多的东西了,比如说异步dispose,using。在此肯定整理不完,后续另外一节补齐。

后一节,异步。

posted @ 2020-07-26 10:12  敖毛毛  阅读(412)  评论(0编辑  收藏  举报