重學c#系列——非託管實例(五)
- 2020 年 7 月 26 日
- 筆記
- c# 基礎(回憶錄)
前言
託管資源到是好,有垃圾回收資源可以幫忙,即使需要我們的一些小小的調試來優化,也是讓人感到欣慰的。但是非託管資源就顯得蒼白無力了,需要程序員自己去設計回收,同樣有設計的地方也就能體現出程序員的設計水平。
託管類在封裝對非託管資源的直接引用或者間接引用時,需要制定專門的規則,確保非託管資源在回收類的一個實例時釋放。
為什麼要確保呢?
是這樣子的,畫一個圖。
上圖中託管中生成並引用非託管,一但非託管和託管中的引用斷開(託管資源被回收),那麼這個時候非託管資源還在,那麼釋放這個問題就有一丟丟困難。
常見的有兩種機制來自動釋放非託管資源。
-
聲明一個構析函數作為一個類的一個成員。
-
在類中實現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#的構析函數去釋放非託管談的也不多。
主要問題如下:
-
無法確認構析函數何時執行,垃圾回收機制不會馬上回收這個對象,那麼也就不會立即執行構析函數。
-
構析函數的實現會延遲該對象在內存中的存在時間。沒有構析函數的對象,會在垃圾回收器中一次處理從內存中刪除,實現構析函數的對象需要兩次。
然後所有對象的終結器是由一個線程來完成的,如果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。在此肯定整理不完,後續另外一節補齊。
結
後一節,異步。