CodeGym /課程 /C# SELF /多執行緒入門

多執行緒入門

C# SELF
等級 55 , 課堂 0
開放

1. 介紹

多執行緒就像辦公室裡幾個人同時做事:一個在印文件、另一個在打電話、第三個在煮咖啡(當然,他們都是程式設計師)。如果只有一個人,他會把所有事情排隊做,辦公室會因為等咖啡而悶到窒息。在程式裡也是一樣:單執行緒程式一次只能做一件事。

想像你的應用在做一個很耗時的操作,比如從網路下載檔案或計算超大的表格。其他東西在那段時間會「凍結」——按鈕按不下去、動畫不動,還會跳出「沒有回應」之類的提示視窗。

多執行緒讓應用能同時做好幾件事:界面保持回應、操作可以並行執行,這樣你就不用對著電腦喊「電腦,你又死當了?!」。

基本概念與術語

在潛水之前,先搞清楚什麼是執行緒(thread)以及它和進程的差別。

  • 進程 (Process): 獨立的程式,有自己的地址空間、變數和資源。例如在 Windows 裡每個被啟動的應用通常是獨立的進程。
  • 執行緒 (Thread): 在進程內的執行單位。進程可以包含一或多個執行緒,這些執行緒會共同使用同一份資源(記憶體、變數)。

為什麼多執行緒會引發那麼多疑問?

因為執行緒是愛冒險的傢伙:它們可能在任何時刻開始執行、把資料搞亂、互相打斷,若不控制好順序,記憶體就會變成災難現場。如果你覺得這像沒大人帶的托兒所——完全沒錯!對執行緒的紀律和注意是寫出可靠多執行緒程式的基礎。

2. C# 與 .NET 中多執行緒的歷史與角色

早期 C# 程式比較單純、也常是單執行緒的。隨著效能要求增加、多核 CPU 的普及,以及需要做出不會卡住介面的應用,.NET 加入了多執行緒的工具。最開始有經典的 System.Threading.Thread 類別,後來出現了 tasks(Task)、非同步方法(async/await)、資料平行處理(PLINQ)以及更多高階的同步原語。

C# 發展成一個強大的平台,在那裡多執行緒不是稀罕玩意,而是日常。

圖示:進程與執行緒

來一個簡單的示意圖:


+--------------------------------------------------+
|                 進程 (你的程式)                  |
|      +-------------+   +-------------+           |
|      |  執行緒 1   |   |  執行緒 2   |           |
|      +-------------+   +-------------+           |
|                ...                                 |
|      +-------------+                              |
|      |  執行緒 N   |                              |
|      +-------------+                              |
+--------------------------------------------------+

同一個進程裡的所有執行緒都能看到共同的變數和資源。

3. 如何在 C# 中建立執行緒?

從最基礎開始:Thread 類別,屬於命名空間 System.Threading

範例:啟動第二個執行緒

假設有一個耗時任務——例如計算從 1 到 10_000_000 的總和。當計算在跑時,主執行緒可以印出對使用者的問候。


using System;
using System.Threading;

class Program
{
    // 第二個任務的方法
    static void CalculateSum()
    {
        long sum = 0;
        for (int i = 1; i <= 10_000_000; i++)
            sum += i;
        Console.WriteLine($"[執行緒 2] 總和: {sum}");
    }

    static void Main()
    {
        // 建立執行緒,傳入方法的 delegate
        Thread thread = new Thread(CalculateSum);
        
        thread.Start(); // 啟動第二個執行緒

        // 主執行緒繼續工作
        Console.WriteLine("[執行緒 1] 嗨!我們在並行工作...");

        // 在退出前等第二個執行緒完成
        thread.Join();

        Console.WriteLine("[執行緒 1] 全部完成!");
    }
}

會發生什麼?
畫面會先出現 "[執行緒 1] 嗨!我們在並行工作...",然後當計算執行緒結束時會輸出總和。

典型問題:誰先輸出

多試幾次執行這段程式——輸出順序可能會不一樣!有時總和先出、有時問候先出。這就是多執行緒的真實面貌——程式變得不那麼可預測,就像星期一貓的心情。

4. 記憶體區域:執行緒能看見什麼?

同一個進程裡的執行緒可以存取相同的變數(除非變數是方法的區域變數)。如果一個執行緒改變變數,其他執行緒也會看到變化!

範例:共享變數


using System;
using System.Threading;

class Program
{
    static int counter = 0;

    static void Increment()
    {
        for (int i = 0; i < 1000; i++)
            counter++;
    }

    static void Main()
    {
        Thread t1 = new Thread(Increment);
        Thread t2 = new Thread(Increment);

        t1.Start();
        t2.Start();

        t1.Join();
        t2.Join();

        Console.WriteLine($"counter = {counter}");
    }
}

我們預期在 counter 看到多少?照理每個執行緒會做 1000 次遞增,應該是 2000

結果不是這樣!多跑幾次會看到不同的數值:1782、1935、1999…
為什麼?這就是經典的 Race Condition ——執行緒在讀取 -> 增加 -> 寫入 的間隙互相搶份,部分遞增被丟失了。

5. 有用的小細節

執行緒如何和 UI 互動?

在現代的桌面應用(WinForms/WPF/MAUI)裡,主執行緒通常負責處理圖形介面。所有使用者事件(點擊、輸入)都在主執行緒處理。背景工作應放到其他執行緒,但依規則不能直接從背景執行緒去「碰」UI,以免造成混亂。

在主控台程式沒這個限制,可以從任何執行緒呼叫 Console.WriteLine。不過在真實應用中,若沒正確同步,介面還是會出問題。

順序與並行

用表格幫你記住差別。

單執行緒程式 多執行緒程式
依序執行任務 任務可能同時執行
長時間任務會讓 UI 卡住 UI 保持回應
讀寫變數很簡單 需要控制對資料的存取
比較容易除錯 除錯可能很麻煩

使用執行緒時的重要事項

  • 共享變數 = 共享風險。 如上例,如果多個執行緒使用同一個變數而不做同步,就會出錯!(後面的課會詳談)
  • 可以用 Join() 等待執行緒。 Join() 會把呼叫它的執行緒暫停,直到目標執行緒結束。如果你需要等結果就用它。
  • Thread 不能重複啟動。 執行緒結束後不能再用同一個物件啟動——要新建一個 Thread
  • 結束執行緒。 執行緒會在其方法執行完畢時終止。強制「殺死」執行緒通常是壞主意(像 Abort() 這種方法已被認為有害且過時)。

多執行緒在實務上的用途

  • UI 應用:避免在背景載入或計算時凍結介面。
  • 伺服器與服務:同時處理大量客戶端的請求。
  • 高效能計算:把大任務(例如處理上百萬筆資料)切成小塊並行處理。
  • 遊戲、模擬、資料處理:模擬複雜系統同時保持效能。

多執行緒的問題

多執行緒給你力量,也帶來麻煩:

  • Race Condition (狀態競賽): 當多個執行緒同時修改相同資料,結果會依賴指令的執行順序,這個順序是不可預測的。
  • Deadlock(互相鎖定): 執行緒互相等待對方,沒有人能繼續執行。
  • Starvation(飢餓): 某個執行緒持續得不到資源,永遠被剝奪執行機會。

這堂課我們只提到多執行緒的主要問題,接下來的課會教你如何識別與避免它們。現在只要記住:如果程式「凍住」了,造成問題的不一定只是 bug,可能是執行緒在那邊開派對。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION