☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
C# 基礎 進階

🧵 多執行緒與平行處理

📌 什麼是多執行緒?

想像你在一家餐廳工作。單執行緒就像只有一個廚師,要一道一道菜做。多執行緒就像有多個廚師,可以同時做不同的菜,大幅提升效率。


📌 Thread 基礎

// 建立一個新的執行緒(雇一個新廚師)
Thread thread = new Thread(() => // 用 Lambda 定義要執行的工作
{
    // 模擬耗時的工作
    for (int i = 0; i < 5; i++) // 迴圈 5 次
    {
        Console.WriteLine($"背景執行緒:{i}"); // 印出目前進度
        Thread.Sleep(1000); // 暫停 1 秒(模擬耗時工作)
    }
});

thread.IsBackground = true; // 設定為背景執行緒(主程式結束時會自動停止)
thread.Start(); // 啟動執行緒(新廚師開始工作)

// 主執行緒繼續做自己的事
Console.WriteLine("主執行緒繼續執行"); // 這行會馬上印出

thread.Join(); // 等待背景執行緒完成(等新廚師做完才繼續)
Console.WriteLine("所有工作完成"); // 背景執行緒做完後才印出

📌 ThreadPool 與 Task.Run

// ThreadPool:由系統管理的執行緒池(像是外包的廚師團隊)
ThreadPool.QueueUserWorkItem(state => // 把工作丟給執行緒池
{
    // 在執行緒池中執行工作
    Console.WriteLine($"ThreadPool 執行緒 ID:{Thread.CurrentThread.ManagedThreadId}");
});

// Task.Run:現代化的做法(推薦使用)
Task task = Task.Run(() => // 把工作丟給背景執行緒執行
{
    // 模擬耗時計算
    int result = 0; // 初始化結果
    for (int i = 0; i < 1000000; i++) // 計算一百萬次
    {
        result += i; // 累加
    }
    Console.WriteLine($"計算結果:{result}"); // 印出結果
});

// Task.Run 搭配回傳值
Task<int> taskWithResult = Task.Run(() => // 泛型版本可以回傳值
{
    // 模擬耗時計算
    Thread.Sleep(2000); // 暫停 2 秒
    return 42; // 回傳計算結果
});

int answer = taskWithResult.Result; // 取得結果(會阻塞直到完成)
Console.WriteLine($"答案是:{answer}"); // 印出 42

📌 同步機制:保護共享資源

lock 關鍵字

// 沒有 lock 的危險情況:多個廚師搶同一把刀
public class Counter
{
    private int _count = 0; // 共享的計數器
    private readonly object _lockObj = new object(); // 鎖定物件(像是一把鑰匙)

    // 安全的遞增方法
    public void SafeIncrement()
    {
        lock (_lockObj) // 進入前先拿到鑰匙(一次只有一個執行緒能進入)
        {
            _count++; // 安全地修改共享資源
        } // 離開時自動歸還鑰匙
    }

    // 取得目前計數
    public int GetCount()
    {
        lock (_lockObj) // 讀取時也要鎖定,確保讀到正確的值
        {
            return _count; // 回傳計數值
        }
    }
}

// 使用範例
Counter counter = new Counter(); // 建立計數器

// 建立多個 Task 同時遞增
Task[] tasks = new Task[10]; // 準備 10 個任務
for (int i = 0; i < 10; i++) // 迴圈建立任務
{
    tasks[i] = Task.Run(() => // 每個任務都會遞增 1000 次
    {
        for (int j = 0; j < 1000; j++) // 迴圈 1000 次
        {
            counter.SafeIncrement(); // 安全地遞增
        }
    });
}

Task.WaitAll(tasks); // 等待所有任務完成
Console.WriteLine($"最終計數:{counter.GetCount()}"); // 應該印出 10000

Monitor、Mutex、SemaphoreSlim

// Monitor:跟 lock 類似,但提供更多控制
object monitorObj = new object(); // 建立監視器物件
bool lockTaken = false; // 追蹤是否成功取得鎖定

try
{
    Monitor.TryEnter(monitorObj, TimeSpan.FromSeconds(5), ref lockTaken); // 嘗試在 5 秒內取得鎖定
    if (lockTaken) // 如果成功取得鎖定
    {
        // 安全地存取共享資源
        Console.WriteLine("已取得鎖定,執行工作中...");
    }
    else // 如果 5 秒內沒取得鎖定
    {
        Console.WriteLine("無法取得鎖定,跳過"); // 超時處理
    }
}
finally
{
    if (lockTaken) // 如果有取得鎖定
    {
        Monitor.Exit(monitorObj); // 釋放鎖定
    }
}

// SemaphoreSlim:限制同時存取的數量(像是停車場的車位)
SemaphoreSlim semaphore = new SemaphoreSlim(3); // 最多允許 3 個執行緒同時進入

async Task AccessResource(int id)
{
    await semaphore.WaitAsync(); // 等待取得一個「車位」
    try
    {
        Console.WriteLine($"執行緒 {id} 進入(剩餘車位:{semaphore.CurrentCount})"); // 印出進入訊息
        await Task.Delay(2000); // 模擬使用資源 2 秒
    }
    finally
    {
        semaphore.Release(); // 離開時歸還「車位」
        Console.WriteLine($"執行緒 {id} 離開"); // 印出離開訊息
    }
}

📌 Parallel 與 PLINQ

// Parallel.ForEach:平行處理集合中的每個元素
List<string> urls = new List<string> // 要處理的 URL 清單
{
    "https://example1.com", // 第一個網址
    "https://example2.com", // 第二個網址
    "https://example3.com"  // 第三個網址
};

Parallel.ForEach(urls, url => // 平行處理每個 URL
{
    // 每個 URL 會在不同的執行緒上同時處理
    Console.WriteLine($"正在處理 {url}(執行緒 {Thread.CurrentThread.ManagedThreadId})");
    // 模擬下載
    Thread.Sleep(1000); // 模擬耗時 1 秒
});

// PLINQ:用 LINQ 語法做平行查詢
List<int> numbers = Enumerable.Range(1, 1000000).ToList(); // 建立一百萬個數字

// 用 AsParallel() 把 LINQ 變成平行版本
var evenSquares = numbers
    .AsParallel() // 啟用平行處理
    .Where(n => n % 2 == 0) // 篩選偶數(平行執行)
    .Select(n => n * n) // 計算平方(平行執行)
    .ToList(); // 收集結果

Console.WriteLine($"偶數平方的數量:{evenSquares.Count}"); // 印出結果數量

📌 async vs 多執行緒的差異

// 多執行緒(Threading):建立新的工人去做事
// 適合:CPU 密集的計算(壓縮、加密、數學運算)
Task.Run(() =>
{
    // 這裡會在另一個執行緒上執行
    // 適合做大量計算
    double result = 0;
    for (int i = 0; i < 10000000; i++) // 大量計算
    {
        result += Math.Sqrt(i); // CPU 密集的工作
    }
});

// async/await:不建立新工人,而是讓現有工人在等待時去做別的事
// 適合:I/O 操作(網路請求、檔案讀寫、資料庫查詢)
async Task<string> FetchDataAsync()
{
    using HttpClient client = new HttpClient(); // 建立 HTTP 客戶端
    // await 時執行緒可以去做別的事(不會閒置等待)
    string data = await client.GetStringAsync("https://api.example.com/data"); // 等待網路回應
    return data; // 回傳資料
}

// 簡單比喻:
// 多執行緒 = 雇更多廚師來做菜(增加人力)
// async/await = 一個廚師在等水燒開的時候去切菜(提高效率)

🤔 我這樣寫為什麼會錯?

❌ 錯誤 1:死結(Deadlock)

// ❌ 錯誤寫法:兩個執行緒互相等待對方的鎖
object lockA = new object(); // 鎖 A
object lockB = new object(); // 鎖 B

// 執行緒 1:先拿 A 再拿 B
Task task1 = Task.Run(() =>
{
    lock (lockA) // 取得鎖 A
    {
        Thread.Sleep(100); // 短暫等待,增加死結機率
        lock (lockB) // 嘗試取得鎖 B → 💥 但鎖 B 被執行緒 2 拿走了!
        {
            Console.WriteLine("執行緒 1 完成"); // 永遠不會執行
        }
    }
});

// 執行緒 2:先拿 B 再拿 A(順序相反)
Task task2 = Task.Run(() =>
{
    lock (lockB) // 取得鎖 B
    {
        Thread.Sleep(100); // 短暫等待
        lock (lockA) // 嘗試取得鎖 A → 💥 但鎖 A 被執行緒 1 拿走了!
        {
            Console.WriteLine("執行緒 2 完成"); // 永遠不會執行
        }
    }
});
// 兩個執行緒互相等待,永遠不會結束 → 死結!
// ✅ 正確寫法:統一鎖定順序
object lockA = new object(); // 鎖 A
object lockB = new object(); // 鎖 B

// 所有執行緒都按照相同順序取得鎖(先 A 後 B)
Task task1 = Task.Run(() =>
{
    lock (lockA) // 先取得鎖 A
    {
        lock (lockB) // 再取得鎖 B
        {
            Console.WriteLine("執行緒 1 完成"); // ✅ 可以正常執行
        }
    }
});

Task task2 = Task.Run(() =>
{
    lock (lockA) // ✅ 也是先取得鎖 A(相同順序)
    {
        lock (lockB) // 再取得鎖 B
        {
            Console.WriteLine("執行緒 2 完成"); // ✅ 可以正常執行
        }
    }
});

解釋: 死結就像兩個人在窄巷中面對面,都堅持對方先讓路,結果誰都走不了。解決方法:規定大家都靠右走(統一順序)。

❌ 錯誤 2:競爭條件(Race Condition)

// ❌ 錯誤寫法:多個執行緒同時修改共享變數,沒有保護
int sharedCounter = 0; // 共享的計數器

Task[] tasks = new Task[10]; // 建立 10 個任務
for (int i = 0; i < 10; i++) // 迴圈建立任務
{
    tasks[i] = Task.Run(() =>
    {
        for (int j = 0; j < 10000; j++) // 每個任務遞增一萬次
        {
            sharedCounter++; // ❌ 不是原子操作,會產生競爭條件
        }
    });
}

Task.WaitAll(tasks); // 等待所有任務完成
Console.WriteLine($"結果:{sharedCounter}"); // ❌ 結果會小於 100000!
// ✅ 正確寫法:使用 Interlocked 進行原子操作
int sharedCounter = 0; // 共享的計數器

Task[] tasks = new Task[10]; // 建立 10 個任務
for (int i = 0; i < 10; i++) // 迴圈建立任務
{
    tasks[i] = Task.Run(() =>
    {
        for (int j = 0; j < 10000; j++) // 每個任務遞增一萬次
        {
            Interlocked.Increment(ref sharedCounter); // ✅ 原子操作,保證執行緒安全
        }
    });
}

Task.WaitAll(tasks); // 等待所有任務完成
Console.WriteLine($"結果:{sharedCounter}"); // ✅ 正確印出 100000

解釋: ++ 操作其實分成三步:讀取、加一、寫回。兩個執行緒可能同時讀到相同的值,各自加一後寫回,導致只加了一次。就像兩個人同時從提款機領錢,可能只扣了一次帳。

❌ 錯誤 3:在背景執行緒存取 UI

// ❌ 錯誤寫法(WinForms/WPF):從背景執行緒直接修改 UI
Task.Run(() =>
{
    // 在背景執行緒做完計算後...
    string result = "計算完成"; // 計算結果
    label1.Text = result; // 💥 InvalidOperationException!不能從非 UI 執行緒修改控制項
});
// ✅ 正確寫法:使用 Invoke 切回 UI 執行緒
Task.Run(() =>
{
    // 在背景執行緒做完計算後...
    string result = "計算完成"; // 計算結果

    // 使用 Invoke 切回 UI 執行緒來更新介面
    this.Invoke(() =>
    {
        label1.Text = result; // ✅ 在 UI 執行緒上安全地修改控制項
    });
});

// 或者更好的做法:使用 async/await(自動回到 UI 執行緒)
async void Button_Click(object sender, EventArgs e)
{
    // await 之後會自動回到 UI 執行緒
    string result = await Task.Run(() =>
    {
        // 背景計算
        return "計算完成"; // 回傳結果
    });

    label1.Text = result; // ✅ 自動在 UI 執行緒上執行
}

解釋: UI 控制項只能由建立它的執行緒(UI 執行緒)來修改。就像餐廳的點餐系統只能由前台操作,廚師(背景執行緒)做好菜要透過前台(Invoke)才能送到客人桌上。

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊