重學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中並不會,等了幾分鐘沒有反應。

原因可以在:

//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。在此肯定整理不完,後續另外一節補齊。

後一節,異步。