同事的 .NET 程式抓到一隻有趣的 Bug。以範例程式重現如下:

static void DoProcess(int idx)
{
    while (StartFlag)
    {
        Thread.Sleep(1000);
        Console.WriteLine(
            $"{DateTime.Now:mm:ss} Thread {idx} is running.");
    }
}
 
static void Main(string[] args)
{
 
    for (int i = 1; i <= 4; i++)
    {
        var thd = new Thread(() =>
        {
            DoProcess(i);
        });
        thd.Start();
    }
    StartFlag = true; 
    //三秒後關閉StartFlag
    Thread.Sleep(3000);
    StartFlag = false;
    Console.ReadLine();
}

程式跑迴圈啟動四個 Thread,各 Thread 以 while (StartFlag) { … } 持續執行,沒什麼事要做就每隔一秒 Console.WriteLine() 時間與序號充數。這段程式犯了一個錯,沒在 Thread.Start() 前把 StartFlag 設好,跑完迴圈才 StartFlag = true,導致 DoProcess 什麼都沒做就收工。但如果只是這樣,Bug 馬上會被掀出來,也不會有這篇筆記了。

有趣的現象是 4 條 Thread 中還是有一條 Thread 會跑,使人被「為什麼明明起了 4 條 Thread 卻只有一條執行?」所迷惑:

這個現象源自多執行緒平行執行的時機問題,Thread.Start() 後,主線程式碼會繼續跑下去,而另起 的 Thread 隨後啟動。故推敲實際狀況應為:跑迴圈啟動第一條 Thread,因 StartFlag 為 false 直接結束,啟動第二條 Thread、第三條 Thread 也直接結束,直到第四條 Thread.Start(),進入 DoProcess 之前,主執行緒結束迴圈繼續往下跑執行 StartFlag = true,接著第四條 Thread 才進入 DoProcess() 執行 while (StartFlag),此時 StartFlag 已是 true,因此只有最後一條 Thread 成功運作。

要修正問題,StartFlag = true 應移至 for 迴圈之前:

static void Main(string[] args)
{
    StartFlag = true; //Thread開始前應設定好
 
    for (int i = 1; i <= 4; i++)
    {
        var thd = new Thread(() =>
        {
            DoProcess(i);
        });
        thd.Start();
    }
 
    //三秒後關閉StartFlag
    Thread.Sleep(3000);
    StartFlag = false;
    Console.ReadLine();
}

修改後,四條 Thread 都起來了,但有個問題,編號怎麼是 3 3 4 5,不是 1 2 3 4?

多執行幾次,會發現數字非固定值,有時會是 3 3 5 5。

這一樣與各 Thread DoProcess() 執行時機有關,for 的過程 i 值會歷經 1 2 3 4 5 五種狀態,端看 DoProcess(i) 執行的當下 i 是多少而定。

static void Main(string[] args)
{
    StartFlag = true; //Thread開始前應設定好
 
    for (int i = 1; i <= 4; i++)
    {
        //另外宣告變數,形成Closure
        var idx = i;
        var thd = new Thread(() =>
        {
            DoProcess(idx);
        });
        thd.Start();
    }
 
    //三秒後關閉StartFlag
    Thread.Sleep(3000);
    StartFlag = false;
    Console.ReadLine();
}

要解決這個問題,我們可另外宣告一個變數 idx,透過 Closure 技巧讓四次迴圈中的匿名方法 () => { DoProcess(idx); } 擁有專屬變數,與 i 的變動脫鉤。(延伸閱讀:Closure in C#

抓一隻 Bug 溫習兩種觀念,很划算,呵~


Comments

Be the first to post a comment

Post a comment