淺談C#託管程式中的資源釋放問題

哈哈哈哈哈我撒發表於2009-09-29

便於對文章的開展,需要先明確兩個概念。

    第一個就是很多人用.Net寫程式,會談到託管這個概念。那麼.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一次輪循完成,即需要兩次輪循; 相對於後者,則只是回收記憶體而已。
    很明顯得知,對於某個具體的資源,無法確切知道,物件解構函式什麼時候被呼叫,以及GC什麼時候會去釋放和回收它所佔用的記憶體。那麼對於從C、C++之類語言轉換過來的程式設計師來說,這裡需要轉變觀念。
    那麼對於程式資源來說,我們應該做些什麼,以及如何去做,才能使程式效率最高,同時佔用資源能儘快的釋放。前面也說了,資源分為兩種,託管的記憶體資源,這是不需要我們操心的,系統已經為我們進行管理了那麼對於非託管的資源,這裡再重申一下,就是Stream,資料庫的連線,GDI+的相關物件,還有Com物件等等這些資源,需要我們手動去釋放。 
    如何去釋放,應該把這些操作放到哪裡比較好呢。.Net提供了三種方法,也是最常見的三種,大致如下:
1.解構函式;
2.繼承IDisposable介面,實現Dispose方法;
3.提供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!
   
    顯然物件的解構函式沒有被呼叫。通過如上的實驗以及文字說明,大家會得到如下的一個對比表格。
 
解構函式
Dispose 方法
Close 方法
意義
銷燬物件
銷燬物件
關閉物件資源
呼叫方式
不能被顯示呼叫,會被 GC 呼叫
需要顯示呼叫
或者通過 using 語句
需要顯示呼叫
呼叫時機
不確定
確定,在顯示呼叫或者離開 using 程式塊
確定,在顯示呼叫時
那麼在定義一個型別的時候,是否一定要給出這三個函式地實現呢。
    我的建議大致如下。
    1.提供解構函式,避免資源未被釋放,主要是指非記憶體資源;
    2.對於Dispose和Close方法來說,需要看所定義的型別所使用的資源(參看前面所說),而決定是否去定義這兩個函式;
    3.在實現Dispose方法的時候,一定要加上“GC.SuppressFinalize( this )”語句,避免再讓GC呼叫物件的解構函式。
    C#程式所使用的記憶體是受託管的,但不意味著濫用,好地程式設計習慣有利於提高程式碼的質量以及程式的執行效率。

相關文章