Posts tagged with C#

An Example of BackgroundWorker

其實這篇文章跟上一篇是有點關聯的…… 我為了解決 Memory Leak ,抱著「反正我本來就還有另一個問題要靠 BackgroundWorker 來解決,不如碰碰運氣」的心態,將耗時又耗記憶體的那段程式改寫為 BackgroundWorker ,結果真的有用耶~ 所以我沒有嘗試 GC.Collect() ,因為用這種有副作用的 function 會覺得不太漂亮 :p

BackgroundWorker 的效果跟 Thread 差不多,但更容易使用,方法如下:

  1. 把 BackgroundWorker 元件拉進來,取名為 BGWorker1
  2. 將 WorkerReportsProgress 設為 True ,讓 BackgroundWorker 回報進度。
  3. 雙擊 DoWork 事件,然後如下撰寫:

    private void BGWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        // 另外根據 sender 建立一個 BackgroundWorker
        BackgroundWorker bw = sender as BackgroundWorker;
    
        // 取出參數,我不知道怎麼改成能夠取多參數的版本… 此例中 YourFunction 的第二個參數是 int
        int arg = (int)e.Argument;
    
        // 開始執行你要的工作
        e.Result = YourFunction(bw, arg);
    
        // 處理使用者按下「取消」的動作
        if (bw.CancellationPending)
        {
            e.Cancel = true;
        }
    }
    
  4. 雙擊 RunWorkerCompleted 事件,然後如下撰寫:

    private void BGWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (e.Cancelled)
        {
            // 使用者中止了執行
        }
        else if (e.Error != null)
        {
            // 執行中發生錯誤,可用 e.Error.Message 得到錯誤訊息
        }
        else
        {
            // 執行作業正常完成
        }
    }
    
  5. 雙擊 ProgressChanged 事件,然後如下撰寫:

    private void BGWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        // 用 e.ProgressPercentage 可以取得目前執行進度百分比(0-100)
    }
    

    比較實用的範例:

    private long begin_tick = 0L;
    
    private void BGWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        // 取得工作開始的時間,記得要在 RunWorkerCompleted 中將 begin_tick 設回 0L
        // C# 中有 function scope 的 static variable 嗎?
        if (begin_tick.Equals(0L))
        {
            begin_tick = DateTime.Now.Ticks;
        }
    
        // 計算已執行時間
        TimeSpan elapse_time = new TimeSpan(DateTime.Now.Ticks - begin_tick);
    
        // 預估完整執行所需時間
        long total_tick = elapse_time.Ticks * 100L;
        // 避免 Division by Zero…… 這有更方便的寫法嗎?
        if (!e.ProgressPercentage.Equals(0))
        {
            total_tick /= e.ProgressPercentage;
        }
    
        // 預估剩餘時間
        TimeSpan remain_time = new TimeSpan(total_tick - elapse_time.Ticks);
    
        // 預估完成時間
        DateTime finish_time = new DateTime(DateTime.Now.Ticks + remain_time.Ticks, DateTimeKind.Local);
    
        ProgressBar.Value = e.ProgressPercentage;
        StatusLabel.Text = String.Format("{0}% (經過時間:{1:00}:{2:00}:{3:00} / 預估剩餘:{4:00}:{5:00}:{6:00} / 預估完成:{7})", e.ProgressPercentage, elapse_time.Hours, elapse_time.Minutes, elapse_time.Seconds, remain_time.Hours, remain_time.Minutes, remain_time.Seconds, finish_time.ToString("yyyy-MM-dd HH:mm:ss"));
    }
    
  6. 至於 YourFunction 則類似這樣:

    private void YourFunction(BackgroundWorker bw, int progress)
    {
        if (!BGWorker1.CancellationPending)
        {
            // 執行你要執行的作業
    
            // 回報目前執行進度百分比(0-100),沒錯,你必須自己計算
            BGWorker1.ReportProgress(progress);
        }
    }
    

    傳入參數是陣列,使用 for 迴圈的範例:

    private void YourFunction(BackgroundWorker bw, string[] argument)
    {
        int count = argument.Length;
        for (int i = 0; i < count; i++) {
            if (!BGWorker1.CancellationPending)
            {
                using (SomeClass obj = new SomeClass(argument[i])) {
                    BGWorker1.ReportProgress(i * 100 / count);
                }
            }
        }
    }
    

20080813 ~weka

明天是已經延了一週的 meeting ,而我今天卻還沒有進度可以報告,所以我決定今天到 Lab 努力一天。最近進度會如此緩慢,「沒有方向」是其中一個原因,不過兩天前當我去跟熊催討決策樹程式時,我發現熊已經難以壓抑、幾乎要爆炸了,所以我就想:「嗯… 我還是自力更生吧!」。

今晚我本來的目標只是要實作完我的計分方法,並且找出計分方法中的問題,不過修改演算法好難,我改了一陣子就感到腸枯思竭,於是決定先研究決策樹。我去把 Weka 抓下來,然後照著 IKVM with Weka tutorial 這篇教學作,意外地非常簡單,而且因為 IKVM 用 Reflection 實作了 IntelliSense ,所以在 Visual C# 裡寫起 Java 就跟寫 C# 一樣自然, IntelliSense 不只是能減少打字的麻煩,某種程度上,它可以讓寫程式就像是一連串的選擇題 XD

不過最後我還是沒有在 C# 裡面寫 Java ,因為我打開 Weka 之後發現…… 我完全可以在 Weka 裡面完成我的實驗,不需要再寫一個新的、專門化但是功能較少的 GUI 。發現了這點之後,我便又回到計分方法的實作,並且將我現有的程式與計分模組整合,然後產生出資料丟給 Weka 。 Weka 竟然可以直接幫我作 K-fold cross-validation ,連 Recall、Precision、F-measure 也一併算給我,實在是超方便!結果今天的進度很意外地就超前了好多,幾乎算是已經把實驗作完,只剩下寫論文了 XD

20080420 ~laboratorio elämä, makuusali uni

今天又是一個日夜顛倒的日子。

晚上醒來後,猴子說想找人陪吃宵夜,然而線上的同學們意願並不是太高,所以考慮、討論了非常久,久到我都想說我可不可以自己先去吃 XD 最後決定是到惠阿姨吃滷味。這是我到東華以來第一次吃滷味,吃起來的感覺呢,就跟我多年前吃過的滷味差不多 XD 我今天點的好像是花干、凍豆腐和科學麵,性質過於相近,吃起來感覺都一樣,真是選擇錯誤… 囧rz

用完餐後我到 Lab 念 paper ,本來我想把車停在行政大樓旁邊的小停車場,但停那裡的話早上就得再來移位,所以最後決定停在前門。有了一次經驗後才發現,停在這裡比工學院旁邊好很多,從 Lab 到工學院這段路雖然比較短,但是照明不佳;而從前門一直到 Lab 這一段路的夜間照明都非常好,所以以後要來 Lab 值大夜班的話都會停這裡吧!

今天半夜的 Lab 人還不少,平常不會來值大夜的庭任、卓哥都待在這裡,聽卓哥說他是要等著考早上的 ITE ,有必要這麼拼嗎? XD

今天念的是有關網頁文件分類的 paper ,雖然作者是個大頭,但是性質跟目前 Lab 的走向差有點多,讓我唸得不太踏實。途中多次想換另一篇來念,甚至還真的開始畫起另一篇 paper 的心智圖,然而經過這些波折之後,我還是看完了這篇網頁文件分類的 XD 這篇大概是我看得最快的一篇了吧!時間壓力真是恐怖。

由於前面提過的原因,我這篇 paper 唸得滿心虛的,因此就順手找了一些 E-mail corpora 來充實 Lab 的收藏,以彌補我心中的不安 XD 在蒐集這些 corpora 時,我發現其中有個 corpora 包含正常郵件資料,並且沒有經過編碼,適合我們使用,因此我拿出好久沒更新的 Herbivora 來 run ,然後又 run 出了 bug… 囧rz

這又是一個出在 RegEx 上的 bug ,而且還是出在最複雜的 E-mail RegEx 上… 所以我只好再來 debug 。搜尋了一下 “C# RegEx bug” ,發現不只有我發生這個問題,不過微軟說這是因為我的 RegEx 寫得不好,解決方法則是修改 RegEx… 所以我就乖乖地修改了,今天修改以前我還先查了一些 RegEx 教學,我覺得我現在的 RegEx 功力又進步了喔!1今天學會了給 Group 取名字,這樣可以讓 RegEx 容易閱讀2,在取值時也比較方便 :)

我在教學上看到 .NET 的 group 取名後,可以使用 \k<name> 的方式呼叫,這樣就不用 copy-paste 相同的 RegEx ,不過這個功能我還沒測試出來,要再研究看看~


  1. 因為進步空間很大 XD 

  2. 之前的版本完全不會想要維護它,每次修改都是砍掉重練。 

20080323 ~aho-corasick

昨天用 Aho-Corasick 比較慢果然是因為我使用錯誤,而今天重寫後成功了,執行時間幾乎沒有差異1,甚至在郵件量小時, Aho-Corasick 還會輸在它必須先為 Keywords 建立 tree 而花費更多時間,然而我還是很開心地改用 Aho-Corasick——因為我發現我本來使用的 for-loop + String.Contains 就是吃掉記憶體的最大兇手。之前我用 CLR Profiler 檢查時有發現在 Herbivora 中吃記憶體最嚴重的是 System.String 物件,但是我在建立字串時都有記得使用 StringBuilder ,所以一直搞不懂這是為什麼,沒想到今天拿掉 String.Contains ,改用 Aho-Corasick 後,記憶體用量馬上從 300 MB 掉到 60 MB2,所以就算 Aho-Corasick 慢一點點,我還是會用它的啦! XD

雖然根據我的需求, Aho-Corasick 跟 String.Contains 幾乎沒差,但理論上 Aho-Corasick 還是會比 String.Contains 快。今天在我將它套進 Herbivora 前先作了 benchmark ,發現速度是 Aho-Corasick ≈ String.Contains > String.IndexOf ≫ Regex , String.Contains 的速度大約比 Aho-Corasick 慢三倍3, String.IndexOf 由於還需回傳在哪裡找到,當然會比 String.Contains 慢一點, Regex 則又慢更多。

接下來聽說 Dictionary 通常會比 Hashtable 快,所以我想改用 Dictionary 。我本來就偏好使用 type-safe 的 Generic Collections ,這樣要取資料時也可以省下 type cast 的花費 :)

雖然 Dictionary 和 Hashtable 的 Method 都一樣,但行為有些不同,當我要用一個不存在的 key 來取 value 時, Hashtable 會回傳 null ,而 Dictionary 會給我 KeyNotFoundException ,我上次是用 try-catch 來處理這個 Exception ,但我沒想到 try-catch 這麼貴,昨天 Aho-Corasick 的速度會大輸,我想這也是其中一個原因…

下一步想測測看加入 q-Gram 會不會有更好的效果(用記憶體換速度 XD)


  1. 處理大量資料(57,000 封郵件)時也只快 4% 。 

  2. 這是處理大量資料時的情況,並非平時用量都這麼大。 

  3. 這只是為了方便說明而使用的某次實驗結果,實際倍數會因為 patterns 數量及 subject 長度而改變。 

Memory Leak in C#

Dispose() do not free memory instantly

.NET 中有自動記憶體管理機制,稱為 GC 。在下的觀念還不是很清楚,所以這篇不敢談太多背後的原理 :p

GC 讓 Programmer 不能直接管理記憶體,在我看來這應該能稱作「將記憶體管理抽象化」。 GC 並不會在「將變數設為 NULL」或「呼叫某物件的 Destructor」時馬上歸還被佔用的記憶體,而會等到某個不明的條件1發生時才啟動回收機制。但也並不是說將變數設為 NULL 或呼叫 Destructor 完全沒用,當某個條件發生時,這些變數、物件所佔用的記憶體便會被釋出。

為了確保 Desturctor 釋放出該釋放的資源,我們可以實作 IDisposable 介面:

private bool disposed = false;

protected virtual void Dispose(bool disposing)
{
    if (!disposed)
    {
        if (disposing)
        {
            // Release managed resources
        }
        // Release unmanaged resources
    }

    disposed = true;
}

public void Dispose()
{
    Dispose(true);
    GC.SuppressFinalize(this);
}

~YourClass()
{
    Dispose(false);
}

實作了 IDisposable 介面之後,便可以在使用完物件時呼叫 obj.Dispose() 或者是使用 using 來將資源標記為「下次可以被釋放」。 using 的用法如下,這種寫法可以讓 Dispose() 自動地被呼叫:

using(YourClass obj = new YourClass())
{
    // Do something
}

除了 Dispose() 以外, C# 中還有一個名為 Finalize() 的 Method ,但這個 Method 無法被覆寫,所以我沒有多加研究。

不過就算照著上面作完了,記憶體仍然必須等到某個條件發生時才會釋放,如果那個條件一直沒有達到,程式就還是會發生「記憶體不足」的問題。

To GC.Collect(), or not to GC.Collect()

有許多文章說,用 GC.Collect() 可以確實地釋放出記憶體。這裡有人做了實驗,他先將記憶體塞滿,當 OutOfMemoryException 發生時,便呼叫 GC.Collect(GC.MaxGeneration) 強制 GC 釋放資源,另有一篇文章建議寫成,不過根據他測試的結果,這還必須跟 gcServer 這個屬性配合才行。組態檔的寫法如下:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <runtime>
        <gcServer enabled="false" />
    </runtime>
</configuration>

不過 gcServer 該設成 truefalse 則沒有明說。另有一篇文章提到,如果電腦的 CPU 是多核心,則 .NET 預設會採用 true ,也就是 Server Mode 。這種模式下記憶體達到釋放的門檻較高,容易發生門檻還未達到,記憶體便用罄的窘境。設成 false ,使用 Workstation Mode 可獲得改善。然而這篇文章談的是 ASP.NET ,我不確定在 Windows Form 應用程式中這個規則是否也能適用。

如果你寫的是 ASP.NET ,組態檔放在 %WINDIR%\Microsoft.NET\Framework\(版本)\Aspnet.config 。而 Windows Form 的組態檔則視執行檔檔名而定,若你的執行檔檔名是 hello.exe ,組態檔就放在同目錄下的 hello.exe.config

雖然 GC.Collect() 可能能夠解決問題,但必須注意不能過度使用,以免程式效能低落。確定要用 GC.Collect() 的話,有一篇文章提出了建議的寫法:

GC.Collect();
GC.WaitForPendingFinalizer();
GC.Collect();

  1. 對我來說是滿神秘的啦~ XD 

 1 2 3 Next →