同事的 .NET 程式抓到一隻有趣的 Bug。以範例程式重現如下:
staticvoid DoProcess(int idx)
{
while (StartFlag)
{
Thread.Sleep(1000);
Console.WriteLine(
$"{DateTime.Now:mm:ss} Thread {idx} is running.");
}
}
staticvoid 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 迴圈之前:
staticvoid 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 是多少而定。
staticvoid 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 溫習兩種觀念,很划算,呵~