2012年1月27日 星期五

C# Threading Finial

上一篇文章(C# Threading - 使用 WaitHandle)有提到
【主執行緒要等副執行緒查完之後才會更新介面上的資訊感覺很鳥】,
所以今天我要來改善這個問題,
基本上我想到的解法就是把原本查詢按鈕按下去裡面的程式改成另一個獨立的函式,
然後用副執行緒去呼叫,
所以原來的 btnThreadingQuery_Click 就變成下面這樣啦!

private void btnThreadingQuery_Click(object sender, EventArgs e)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(QueryMain));
}

原來裡面的程式就被我移到了 QueryMain() 去了,然後妳還記得嗎?
在第一篇文章(C# 多緒程式該怎麼寫? - Threading)的時候,
我曾經說過副執行緒不可以直接去處理 UI 相關的東西,
所以原本我們在裡面有去控制按鈕跟滑鼠游標也要依照第一篇的觀念去修改,
全部的修改內容如下:

private void QueryMain(object param)
{
    waitHandles = new List<WaitHandle>();

    Cursor tmpCursor = this.Cursor;
    Label[] labels = { lblThreadingDuration, lblThreadingTable1Count,
                         lblThreadingTable1Duration, lblThreadingTable2Count,
                         lblThreadingTable2Duration };
    ResetLabel(labels);
    //Application.DoEvents();

    try
    {
        //btnThreadingQuery.Enabled = false;
        this.Invoke(new UpdateButtonEnableHandler(UpdateButtonEnable),
            new object[] { btnThreadingQuery, false });
        //this.Cursor = Cursors.WaitCursor;
        this.Invoke(new UpdateFormCursorHandler(UpdateFormCursor),
            new object[] { Cursors.WaitCursor });

        CalcTime calcTime = new CalcTime();

        waitHandles.Add(new AutoResetEvent(false));
        object param1 = new object[] { "table1", lblThreadingTable1Count,
            lblThreadingTable1Duration, calcTime, waitHandles[waitHandles.Count()-1]};

        ThreadPool.QueueUserWorkItem(
            new WaitCallback(QueryTableByThreading), param1);

        waitHandles.Add(new AutoResetEvent(false));
        object param2 = new object[] { "table2", lblThreadingTable2Count,
            lblThreadingTable2Duration, calcTime, waitHandles[waitHandles.Count()-1]};

        ThreadPool.QueueUserWorkItem(
            new WaitCallback(QueryTableByThreading), param2);

        WaitHandle.WaitAll(waitHandles.ToArray());

        //lblThreadingDuration.Text = calcTime.Duration();
        this.Invoke(new UpdateLabelTextHandler(UpdateLabelText),
            new object[] { lblThreadingDuration, calcTime.Duration() });
    }
    finally
    {
        //this.Cursor = tmpCursor;
        this.Invoke(new UpdateFormCursorHandler(UpdateFormCursor),
            new object[] { tmpCursor });
        //btnThreadingQuery.Enabled = true;
        this.Invoke(new UpdateButtonEnableHandler(UpdateButtonEnable),
            new object[] { btnThreadingQuery, true });
    }
}

首先要用 ThreadPool.QueueUserWorkItem 來呼叫函式,
所以函式就一定要長得跟它的規定一樣,
所以我們宣告的函式就要長成這樣(如下),

private void QueryMain(object param)

即便我們沒有要傳遞任何參數也是一樣的。

裡面的內容我們修改的不多,
基本上就是有直接異動 UI 的程式我們改成用 this.Invoke 來處理;
所以原本的按鈕跟滑鼠游標就需要修改了。
修改如下:

delegate void UpdateButtonEnableHandler(Button btn, bool enable);

private void UpdateButtonEnable(Button btn, bool enable)
{
    btn.Enabled = enable;
}

delegate void UpdateFormCursorHandler(Cursor c);

private void UpdateFormCursor(Cursor c)
{
    this.Cursor = c;
}

另一個要修改的就是把 Label 初始的那個程式,
原本直接把 Label.Text 指定成 "0" 就要變成用間接的方式去改了,
如下:

private void ResetLabel(Label[] labels)
{
    foreach (Label lb in labels)
    {
        //lb.Text = "0";
        this.Invoke(new UpdateLabelTextHandler(UpdateLabelText),
            new object[] { lb, "0" });
    }
}

也因為這樣,所以原本的 Application.DoEvents 就不需要了。
這樣全部程式就已經都改完了,
感覺是不是很簡單呢。

忘了說,這時候我們已經可以把 QueryTableByThreading 裡面的 are.Set(); 重新移到程式的最末端去了,
現在已經不會發生程式卡住的問題了,
至於原因是甚麼,妳就自己好好想想囉!

現在程式執行起來就跟我想要的一模一樣了,
查詢完 Table2 之後就會先顯示資料筆數跟時間在畫面上了,
而這時候按鈕還是不能按,滑鼠也還是等待的圖案,
然後畫面上也不會顯示【沒有回應】這種鳥鳥的訊息了。


等全部查詢執行完之後,按鈕跟滑鼠就回復原狀,
然後整個查詢時間也正確了!


所以多緒的程式也沒有很難嘛!
如果觀念通了就會覺得很簡單啦!
也許有更簡單的方式吧,不過目前我只知道這樣做囉!

對了!之前有人跟我說 BackgroundWorker 可以直接拿來用,


我之前也有查到,
但是因為查到的資料上面寫說 BackgroundWorker 只能執行一個副執行緒,
而我想要同時多個副執行緒一起執行,
所以 BackgoundWorker 就被我放棄掉啦!
如果妳只有一個副執行緒要處理的話或許 BackgroundWorker 就可以滿足你的要求了吧?
目前我沒有計畫要用 BackgroundWorker 所以妳可能要自己踹踹囉!
聽我同事說 VS2010 好像對 BackgroundWorker 有再加強,
不過我目前手上沒有得試,
改天如果我有踹過我應該也會 po 上來的。
目前這樣應該已經夠用了,
除非我有再發現甚麼須要寫的吧,
不然多緒的程式應該就到此為止了,
接下來就是應用囉!

沒有留言:

張貼留言