• 重学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。在此肯定整理不完,后续另外一节补齐。

    后一节,异步。

  • 相关阅读:
    servlet
    过滤器
    拦截器
    logback
    hibernate(1)
    函数的关键字参数
    函数的不定长参数
    打印星形三角
    九九乘法表
    udp客户端收发数据流程
  • 原文地址:https://www.cnblogs.com/aoximin/p/13377264.html
Copyright © 2020-2023  润新知