前幾天幫同事看一個WinForm問題,明明有Primary Key限制的DataTable,卻冒出數筆PK相同的資料,Grid還會發狂似地不斷捲動。

由於資料的更新動作來自非UI Thread,我們首先懷疑的就是Threading Issue,不過該問題只出現在尖峰時刻資料量爆多的情境下,在測試台中怎麼都無法模擬出來,於是我設計了以下的實驗,證明忽略Thread-Safe Issue時的確會衍生類似的問題。

程式是這樣寫的,Form_Load時建立一個DataTable,並以Symbo欄位l為Primary Key,接著把它指定成C1FlexGrid.DataSource。按下Button時,啟動五條Thread在DataTable中塞入亂數產生的資料,並利用DataTable.Rows.Find的方式檢查該Symbol是否已有資料,決定採取新增或是更新。有兩個重點:
1) Symbol是PK,所以任何兩筆資料的Symbol不可能相同,也不可為NULL
2) DataTable.DefaultView.Sort="Symbol",所以我們看到的資料應該是依Symbol由小到大排序

private DataTable dt = new DataTable();
private string[] symbolPool = new string[100];
 
private void Form1_Load(object sender, System.EventArgs e)
{
    //建立資料表
    dt.Columns.Add("Symbol", typeof(string));
    dt.Columns.Add("Price", typeof(float));
    dt.Columns.Add("Quantity", typeof(int));
    dt.Columns.Add("Amount", typeof(int));
    //Symbol是Primary Key
    dt.PrimaryKey = new DataColumn[] { dt.Columns["Symbol"] };
    //建立代號清單
    for (int i = 0; i < symbolPool.Length; i++)
        symbolPool[i] = i.ToString("0000");
    //依代號排序
    dt.DefaultView.Sort = "Symbol";
    c1.DataSource = dt;
}
 
private void btnGo_Click(object sender, System.EventArgs e)
{
    if (btnGo.Text=="GO") 
    {
        bStop=false;
        //開啟五條Thread同時塞資料
        for (int i=0; i<5; i++) 
        {
            System.Threading.ThreadPool.QueueUserWorkItem(
                new WaitCallback(updateTrade));
        }
        btnGo.Text="Cancel";
    }
    else 
    {
        bStop = true;
        btnGo.Text = "GO";
    }
 
}
//利用bStop旗標要求停止測試
private bool bStop = false;
 
private void updateTrade(object args) 
{
    Random rnd = new Random();
    while (!bStop) 
    {
        //用亂數產生測試資料
        string symbol = rnd.Next(100).ToString("0000");
        float prz = Convert.ToSingle(
            Math.Round(rnd.NextDouble()*100, 2));
        int qty = rnd.Next(50) * 1000;
        int amt = Convert.ToInt32(prz * qty);
        try 
        {
            //先找看看是否已有該筆資料
            DataRow row = dt.Rows.Find(symbol);
            bool bNew = false;
            //沒有時則準備新增
            if (row==null) 
            {
                row = dt.NewRow();
                bNew = true;
            }
            row["Symbol"]=symbol;
            row["Price"]=prz;
            row["Quantity"]=qty;
            row["Amount"]=amt;
            if (bNew)
                dt.Rows.Add(row);
        }
        catch (Exception ex) 
        {
            Console.WriteLine(ex.Message);
        }
        //等待不定長度的時間
        Thread.Sleep(rnd.Next(200));
    }
}

在.NET 1.1下執行程式,我們得到了這樣的結果

0024, 0030出現兩筆,0066後方排了一筆0030,再下方有一筆完全沒數字的資料。PK重複、PK=NULL、排序反覆,這... 程式造反了嗎? 是的,當程式涉及了多條Thread時,只要稍不留神,就會爆出各式各樣想都想不到的結果。程式再跑一段時間,也許還有機會遇見曾讓我灰頭土臉的Null Reference Exception。

在Multithread環境下使用沒有非Thread-Safe的Class/Method,產生的結果無法預期,而這也不是元件開發者的責任。當使用多個Thread去存取同一個物件時,切記要考慮彼此間的交互影響,而在上面的例子中,我們只需要加一個lock(this)就可解決問題。強制同步化會影響效能,但跑出錯誤的結果,再怎麼快也是快心酸的,二者輕重,不言而喻。

lock (this) 
{
    try 
    {
        DataRow row = dt.Rows.Find(symbol);
        bool bNew = false;
        if (row==null) 
        {
            row = dt.NewRow();
            bNew = true;
        }
        row["Symbol"]=symbol;
        row["Price"]=prz;
        row["Quantity"]=qty;
        row["Amount"]=amt;
        if (bNew)
            dt.Rows.Add(row);
    }
    catch (Exception ex) 
    {
        Console.WriteLine(ex.Message);
    }
}

享受Multithread Coding樂趣之餘,千萬記住: Keep Your Code Thread-Safe!

延伸閱讀: UI Thread Issue


Comments

Be the first to post a comment

Post a comment