重學c#系列——非託管例項(五)

團隊buff工具人發表於2020-07-26

前言

託管資源到是好,有垃圾回收資源可以幫忙,即使需要我們的一些小小的除錯來優化,也是讓人感到欣慰的。但是非託管資源就顯得蒼白無力了,需要程式設計師自己去設計回收,同樣有設計的地方也就能體現出程式設計師的設計水平。

託管類在封裝對非託管資源的直接引用或者間接引用時,需要制定專門的規則,確保非託管資源在回收類的一個例項時釋放。

為什麼要確保呢?

是這樣子的,畫一個圖。

上圖中託管中生成並引用非託管,一但非託管和託管中的引用斷開(託管資源被回收),那麼這個時候非託管資源還在,那麼釋放這個問題就有一丟丟困難。

常見的有兩種機制來自動釋放非託管資源。

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

後一節,非同步。

相關文章