多線程程式設計常被視為高難度領域,但掌握核心概念後其實相當直觀。本文從作業系統線程基礎出發,層層深入講解 C# 線程建立、同步原語、非同步演進,並提供實戰範例與最佳實踐指南。
using System.Threading;
// 傳統 Thread 類別
Thread t1 = new Thread(() =>
{
for (int i = 0; i < 10; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Thread: {i}");
}
});
t1.Name = "WorkerThread";
t1.IsBackground = true; // 重要:主程式結束時自動終止
t1.Start();
// 參數化啟動
Thread t2 = new Thread(DoWork);
t2.Start("Hello"); // 傳遞物件參數
void DoWork(object data)
{
string msg = (string)data;
Console.WriteLine(msg);
}
| 成員 | 說明 |
|---|---|
CurrentThread | 靜態屬性,取得目前執行的線程 |
ManagedThreadId | 唯一識別碼(除錯用) |
IsAlive | 線程是否正在執行 |
IsBackground | 背景線程:不阻止程式結束 |
Priority | 排程優先權 (Lowest~Highest) |
ThreadState | 狀態旗標 |
Sleep(ms) | 靜態方法,讓當前線程休眠 |
Join() | 等待線程結束 |
Abort() | 已過時,強制終止,易造成資源洩漏 |
// 經典競爭條件範例
int counter = 0;
for (int i = 0; i < 10; i++)
{
new Thread(() =>
{
for (int j = 0; j < 10000; j++) counter++;
}).Start();
}
Thread.Sleep(1000);
Console.WriteLine(counter); // 預期 100000,實際通常 < 100000
| 原語 | 命名空間 | 適用場景 | 特點 |
|---|---|---|---|
lock / Monitor | System.Threading | 簡單互斥、臨界區短 | 語法糖、可重入、死鎖風險低 |
Mutex | System.Threading | 跨進程同步 | 核心物件、較重、可命名 |
Semaphore / SemaphoreSlim | System.Threading | 限制並發數量 | 計數器機制、Slim 版輕量 |
AutoResetEvent / ManualResetEvent | System.Threading | 線程間訊號通知 | EventWaitHandle 衍生、等待/通知模式 |
ReaderWriterLockSlim | System.Threading | 讀多寫少 | 支援多讀單寫、效能優於 lock |
Interlocked | System.Threading | 原子操作 | 硬體級原子、無鎖、效能最佳 |
Concurrent 集合 | System.Collections.Concurrent | 執行緒安全集合 | 無鎖/細粒度鎖、開箱即用 |
ManualResetEvent mre = new ManualResetEvent(false); // 初始非發信號狀態
// 背景工作線程
Thread worker = new Thread(() =>
{
Console.WriteLine("Worker: 等待啟動訊號...");
mre.WaitOne(); // 阻塞直到收到訊號
Console.WriteLine("Worker: 收到訊號,開始工作");
for (int i = 0; i < 5; i++)
{
Thread.Sleep(500);
Console.WriteLine($"Worker: 處理中 {i}");
}
Console.WriteLine("Worker: 完成");
});
worker.Start();
// 主線程做其他事...
Thread.Sleep(2000);
Console.WriteLine("Main: 發送啟動訊號");
mre.Set(); // 發送訊號,喚醒所有等待者
// 等待背景線程結束
worker.Join();
Console.WriteLine("Main: 程式結束");
var queue = new BlockingCollection(10); // 容量 10
// 生產者
Task.Run(() =>
{
for (int i = 0; i < 20; i++)
{
queue.Add(i); // 滿時自動阻塞
Console.WriteLine($"Produced: {i}");
Thread.Sleep(100);
}
queue.CompleteAdding(); // 標記完成
});
// 消費者
Task.Run(() =>
{
foreach (var item in queue.GetConsumingEnumerable()) // 空時阻塞,CompleteAdding 後結束
{
Console.WriteLine($"Consumed: {item}");
Thread.Sleep(200);
}
Console.WriteLine("Consumer 結束");
});
// 建立與啟動
Task t1 = Task.Run(() => DoWork()); // ThreadPool 執行
Task t2 = Task.Factory.StartNew(() => DoWork(), TaskCreationOptions.LongRunning); // 專用線程
// 等待與結果
int result = await Task.Run(() => Compute()); // 非同步等待,不阻塞線程
int result2 = Task.Run(() => Compute()).Result; // 同步阻塞等待(避免在 UI 執行緒用)
// 組合
Task all = Task.WhenAll(task1, task2, task3); // 全部完成
Task any = Task.WhenAny(task1, task2); // 任一完成
// 取消
var cts = new CancellationTokenSource();
Task.Run(() => LongWork(cts.Token), cts.Token);
cts.CancelAfter(5000); // 5 秒後取消
async 方法回傳 Task/Task<T>/void (僅事件處理器)await 只能在 async 方法中使用await 會「暫停」方法,釋放線程,完成後續在原上下文(或 ThreadPool)執行.Result、.Wait() 造成死結async/awaitTask.Run 卸載到 ThreadPoolSemaphoreSlim 限制並發度(如同時下載數)CancellationToken 支援取消ConcurrentDictionary 等無鎖集合try/catch 包裹 awaitnew Thread()(除非長時間運行、需專用線程)Thread.Abort()、Thread.Suspend/Resumelock 中執行耗時 IO 或呼叫外部程式碼.Result/.Wait())ConfigureAwait(false)(程式庫開發時)
// 同步版:每個請求佔用一條線程
public string SyncDownload(string url)
{
using (var client = new WebClient())
return client.DownloadString(url); // 阻塞線程
}
// 非同步版:釋放線程處理其他請求
public async Task AsyncDownload(string url)
{
using (var client = new HttpClient())
return await client.GetStringAsync(url); // 釋放線程
}
// 壓測結果(1000 並發請求,每請求 100ms 延遲):
// 同步:需 1000 條線程,記憶體 ~200MB,排程開銷大
// 非同步:~20 條線程,記憶體 ~20MB,吞吐量高 10x+
C# 多線程發展脈絡:Thread → ThreadPool → Task → async/await。核心心法不變:將阻塞操作移出關鍵路徑,善用硬體並行能力。現代開發以 Task-based Asynchronous Pattern (TAP) 為主,搭配 SemaphoreSlim、BlockingCollection、Concurrent* 等高階同步工具,能寫出高效能、可維護、可擴展的並行程式。記住:正確性優於效能,簡單優於巧妙。