2012年1月19日 星期四

C# 多緒程式該怎麼寫? - Threading

之前為了需要寫了一個多緒的程式,
為了怕忘記,所以寫了一個簡單的程式來紀錄一下多緒的程式該怎麼寫,
這樣以後自己才有的參考。

簡單的說就是兩個按鈕,一個是單緒的查詢、一個是多緒的查詢,
兩個查詢的內容基本上是一樣的,先查詢一個筆數很多的資料表,
然後再查一個筆數比較少的資料表。

程式執行的畫面如下:


由於單緒的程式會等第一個查詢完了之後才查第二個資料表,
所以妳會發現第二個查詢的時間其實就等於全部查詢的時間;
但是如果是多緒的程式的話妳就會發現第二個查詢的筆數跟時間會比較快出來,
而全部的執行時間其實只有1毫秒,因為這個只是去呼叫程式的時間,
並不是全部查詢完之後的時間。
(突然發現毫秒的字錯了!!!)

先來看看單緒的程式的寫法吧!


private void btnQuery_Click(object sender, EventArgs e)
{
    Cursor tmpCursor = this.Cursor;
    Label[] labels = { lblDuration, lblTable1Count, lblTable1Duration, lblTable2Count,
                         lblTable2Duration };
    ResetLabel(labels);
    Application.DoEvents();
    try
    {
        btnQuery.Enabled = false;
        this.Cursor = Cursors.WaitCursor;

        CalcTime calcTime = new CalcTime();
        QueryTable("table1", lblTable1Count, lblTable1Duration, calcTime);
        QueryTable("table2", lblTable2Count, lblTable2Duration, calcTime);
        lblDuration.Text = calcTime.Duration();
    }
    finally
    {
        this.Cursor = tmpCursor;
        btnQuery.Enabled = true;
    }
}

程式很簡單,一開始先把顯示資訊的 Label 歸 0,
ResetLabel() 的功用就是這樣簡單而已,
然後呼叫 Application.DoEvents() 讓歸 0 的資訊可以先顯示在畫面上,
如果不寫這行的話會等全部執行完之後才更新畫面。

Label[] labels = { lblDuration, lblTable1Count, lblTable1Duration,
                     lblTable2Count, lblTable2Duration };
ResetLabel(labels);
Application.DoEvents();

呼叫的函式很簡單,
就是查詢傳進來的資料表,
然後更新傳進來的 Label ,
程式如下:

private void QueryTable(string table, Label count, Label duration, CalcTime calcTime)
{
    DataTable dt1 = CommonDBUtility.GetTableFromDB(table);
    count.Text = dt1.Rows.Count.ToString("N");
    duration.Text = calcTime.Duration();
}

單緒寫法的最大壞處就是程式經常會失去回應!


然後資料表2的執行時間也不太正確,
當然我們也可以在執行資料表2的查詢的時候 Restart() 計時器,
不過我們這篇要講的是多緒,
所以接下來我們要來講講如果要改成多緒的話程式該怎麼寫。

我們等等會用下面的程式來將函式丟到執行緒裡面去執行,
所以我們先看看定義吧!

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

第一個參數是要呼叫的函式的名稱,我們等等要呼叫 QueryTableByThreading 。


第二個是函式的參數,不過看起來只接受 object 的型態,
但是我們要傳的參數不只一個,所以要稍微運用一下轉型的技巧,下面會解說。


在用執行緒執行函式之前,我們要先把我們要傳遞的參數變成 object 的型態,如下:


object param2 = new object[] { "table1", lblThreadingTable2Count,
                lblThreadingTable2Duration, calcTime };


然後我們要將查詢跟更新 Label 的程式包在一起變成一個執行緒可以呼叫的形式,
所以我寫了一個新的函式如下:

private void QueryTableByThreading(object param)
{
    object[] oa = (object[])param;

    string table = oa[0].ToString();
    Label count = (Label)oa[1];
    Label duration = (Label)oa[2];
    CalcTime ct = (CalcTime)oa[3];

    DataTable dt = CommonDBUtility.GetTableFromDB(table);
   
    this.Invoke(new UpdateLabelTextHandler(UpdateLabelText),
        new object[] { count, dt.Rows.Count.ToString("N") });

    this.Invoke(new UpdateLabelTextHandler(UpdateLabelText),
        new object[] { duration, ct.Duration() });
}

前面的幾行是將傳進來的 object 轉型成 object[],
然後再一個一個的去取得我們的參數,
接下來就直接呼叫查詢的函式就可以了;

執行緒有一個限制,就是不可以直接異動 UI 的屬性
所以我們要想辦法間接的去異動 UI ,
以便來更新筆數跟執行時間。

我們要先定義一個函式,接受一個 Label 跟 string 的參數,如下:


delegate void UpdateLabelTextHandler(Label lb, string msg);


然後再實做這個函式,
非常簡單,
只是把傳進來的 Label 的 Text 改成傳進來的 string 而已,
如下:


private void UpdateLabelText(Label lb, string msg)
{
    lb.Text = msg;
}


有了這兩樣東西之後我們才可以在執行緒裡面用 Invoke 去執行 UpdateLabelText ,
這邊的 Invoke 會將更新 Label 的程式交給主執行緒去跑


this.Invoke(new UpdateLabelTextHandler(UpdateLabelText),
    new object[] { count, dt.Rows.Count.ToString("N") });


ps.如果主執行緒在忙的時候可能不會被更新到喔!

多緒查詢的按鈕承式如下:

private void btnThreadingQuery_Click(object sender, EventArgs e)
{
    Cursor tmpCursor = this.Cursor;
    Label[] labels = { lblThreadingDuration, lblThreadingTable1Count,
                         lblThreadingTable1Duration, lblThreadingTable2Count,
                         lblThreadingTable2Duration };
    ResetLabel(labels);
    Application.DoEvents();
    try
    {
        btnThreadingQuery.Enabled = false;
        this.Cursor = Cursors.WaitCursor;

        CalcTime calcTime = new CalcTime();

        object param1 = new object[] { "table1", lblThreadingTable1Count,
            lblThreadingTable1Duration, calcTime };

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

        object param2 = new object[] { "table2", lblThreadingTable2Count,
            lblThreadingTable2Duration, calcTime };

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

        lblThreadingDuration.Text = calcTime.Duration();
    }
    finally
    {
        this.Cursor = tmpCursor;
        btnThreadingQuery.Enabled = true;
    }
}


幾乎跟單緒的程式沒甚麼差別,
只差在要先將參數丟到一個 object[] 裡面,
然後再用 ThreadPool.QueueUserWorkItem 的方式來呼叫,
搞懂了應該該就蠻簡單啦!

不過如果妳有注意到多緒的總執行時間好像不太對,
然後副執行緒的查詢還在跑,
但是按鈕卻已經可以再按一次了!

嗯~~~我還沒有找到很好的處理方式,
目前只有兩光法,所以就不介紹了!
等我找到OK的處理方式的時候再 po 上來吧!

1 則留言:

  1. 你好!
    想要尋問 CommonDBUtility.GetTableFromDB class 的寫法。
    我在執行多執行續的時候使用 SqlDataAdapter 不支援多執行續。
    如果我使用 lock 這樣跟我想像的多執行序好像有些出入。

    不知道是否可以指導我一些方向呢!
    我的電子信箱
    deepseacastle@gmail.com

    回覆刪除