問題陳述:
背景:整個程式是C#寫的,其中調用了C++寫的dll。
我這裡是在mobile系統下碰見的一個問題,就是我在C#中調用了一下C#的串口操作,也就是說new一個serialPort對象,然後對串口讀寫,最後Close掉。此時我去調用windowsApi,但不是直接調用,而是通過DllImport一個dll,這個dll是用C++寫的,而dll中有開啟相同串口和讀寫的函數,結果會卡在ReadFile這裡,就是C++裡面的介面函數。
問題分析:
總體感覺是C#資源沒有釋放,雖然Close了,但是資源應該沒有釋放。但是不理解的事,調用C++的CreatFile能成功,返回也有控制代碼,寫串口也能成功,就是卡在ReadFile不動了。不知道具體原因。
於是就查資料,關於C#資源釋放的原理:
首先說一下C#的託管
那麼.Net所指的資源託管到底是什麼意思,是相對於所有資源,還是只限於某一方面資源。很多人對此不是很瞭解,其實.Net所指的託管只是針對記憶體這一個方面,並不是對於所有的資源;因此對於Stream,資料庫的串連,GDI+的相關對象,還有Com對象等等,這些資源並不是受到.Net管理而統稱為非託管資源。而對於記憶體的釋放和回收,系統提供了GC-Garbage Collector,而至於其他資源則需要手動進行釋放。
其次什麼是垃圾
.Net類型分為兩大類,一個就是實值型別,另一個就是參考型別。前者是分配在棧上,並不需要GC回收;後者是分配在堆上,因此它的記憶體釋放和回收需要通過GC來完成。GC的全稱為“Garbage Collector”,顧名思義就是記憶體回收行程,那麼只有被稱為垃圾的對象才能被GC回收。也就是說,一個參考型別對象所佔用的記憶體需要被GC回收,需要先成為垃圾。那麼.Net如何判定一個參考型別對象是垃圾呢,.Net的判斷很簡單,只要判定此對象或者其包含的子物件沒有任何引用是有效,那麼系統就認為它是垃圾。
明確了這兩個基本概念,接下來說說GC的運作方式以及其的功能。記憶體的釋放和回收需要伴隨著程式的運行,因此系統為GC安排了獨立的線程。那麼GC的工作大致是,查詢記憶體中對象是否成為垃圾,然後對垃圾進行釋放和回收。那麼對於GC對於記憶體回收採取了一定的優先演算法進行輪循回收記憶體資源。其次,對於記憶體中的垃圾分為兩種,一種是需要調用對象的解構函式,另一種是不需要調用的。GC對於前者的回收需要通過兩步完成,第一步是調用對象的解構函式,第二步是回收記憶體,但是要注意這兩步不是在GC一次輪循完成,即需要兩次輪循;相對於後者,則只是回收記憶體而已。
那麼對於程式資源來說,我們應該做些什麼,以及如何去做,才能使程式效率最高,同時佔用資源能儘快的釋放。前面也說了,資源分為兩種,託管的記憶體資源,這是不需要我們操心的,系統已經為我們進行管理了;那麼對於非託管的資源,這裡再重申一下,就是Stream,資料庫的串連,GDI+的相關對象,還有Com對象等等這些資源,需要我們手動去釋放。
如何去釋放,應該把這些操作放到哪裡比較好呢。.Net提供了三種方法,也是最常見的三種,大致如下:
<!--[if !supportLists]-->1. <!--[endif]-->解構函式;
<!--[if !supportLists]-->2. <!--[endif]-->繼承IDisposable介面,實現Dispose方法;
<!--[if !supportLists]-->3. <!--[endif]-->提供Close方法。
經過前面的介紹,可以知道解構函式只能被GC來調用的,那麼無法確定它什麼時候被調用,因此用它作為資源的釋放並不是很合理,因為資源釋放不及時;但是為了防止資源泄漏,畢竟它會被GC調用,因此解構函式可以作為一個補救方法。而Close與Dispose這兩種方法的區別在於,調用完了對象的Close方法後,此對象有可能被重新進行使用;而Dispose方法來說,此對象所佔有的資源需要被標記為無用了,也就是此對象被銷毀了,不能再被使用。例如,常見SqlConnection這個類,當調用完Close方法後,可以通過Open重新開啟資料庫連接,當徹底不用這個對象了就可以調用Dispose方法來標記此對象無用,等待GC回收。明白了這兩種方法的意思後,大家在往自己的類中添加的介面時候,不要歪曲了這兩者意思。
首先是這三種方法的實現,大致如下:
/// <summary>
/// The class to show three disposal function
/// </summary>
public class DisposeClass:IDisposable
{
public void Close()
{
Debug.WriteLine( "Close called!" );
}
~DisposeClass()
{
Debug.WriteLine( "Destructor called!" );
}
#region IDisposable Members
public void Dispose()
{
// TODO: Add DisposeClass.Dispose implementation
Debug.WriteLine( "Dispose called!" );
}
#endregion
}
對於Close來說不屬於真正意義上的釋放,除了注意它需要顯示被調用外,我在此對它不多說了。而對於解構函式而言,不是在對象離開範圍後立刻被執行,只有在關閉進程或者調用GC.Collect方法的時候才被調用,參看如下的代碼運行結果。
private void Create()
{
DisposeClass myClass = new DisposeClass();
}
private void CallGC()
{
GC.Collect();
}
// Show destructor
Create();
Debug.WriteLine( "After created!" );
CallGC();
啟動並執行結果為:
After created!
Destructor called!
顯然在出了Create函數外,myClass對象的解構函式沒有被立刻調用,而是等顯示調用GC.Collect才被調用。
對於Dispose來說,也需要顯示的調用,但是對於繼承了IDisposable的類型對象可以使用using這個關鍵字,這樣對象的Dispose方法在出了using範圍後會被自動調用。例如:
using( DisposeClass myClass = new DisposeClass() )
{
//other operation here
}
如上啟動並執行結果如下:
Dispose called!
那麼對於如上DisposeClass類型的Dispose實現來說,事實上GC還需要調用對象的解構函式,按照前面的GC流程來說,GC對於需要調用解構函式的對象來說,至少經過兩個步驟,即首先調用對象的解構函式,其次回收記憶體。也就是說,按照上面所寫的Dispose函數,雖說被執行了,但是GC還是需要執行解構函式,那麼一個完整的Dispose函數,應該通過調用GC.SuppressFinalize(this )來告訴GC,讓它不用再調用對象的解構函式中。那麼改寫後的DisposeClass如下:
/// <summary>
/// The class to show three disposal function
/// </summary>
public class DisposeClass:IDisposable
{
public void Close()
{
Debug.WriteLine( "Close called!" );
}
~DisposeClass()
{
Debug.WriteLine( "Destructor called!" );
}
#region IDisposable Members
public void Dispose()
{
// TODO: Add DisposeClass.Dispose implementation
Debug.WriteLine( "Dispose called!" );
GC.SuppressFinalize( this );
}
#endregion
}
通過如下的代碼進行測試。
private void Run()
{
using( DisposeClass myClass = new DisposeClass() )
{
//other operation here
}
}
private void CallGC()
{
GC.Collect();
}
// Show destructor
Run();
Debug.WriteLine( "After Run!" );
CallGC();
啟動並執行結果如下:
Dispose called!
After Run!
最終的解決方案就是在close後,調用GC.Collect()方法就可以正常使用了。