1. 介紹
現在是時候弄清楚 — Task 和 Thread 本質上有什麼不同?為什麼 C# 多年來建議使用 Task 而不是直接管理 threads?在什麼情況下可以繼續手動使用 threads,什麼時候應該(並且需要)採用以 tasks 為主的做法?
如果你覺得「threads」和「tasks」這兩個詞開始在你腦海的某個陰暗角落混在一起,心跳有點加速 — 別擔心,你不是唯一一個。即使是有經驗的程式設計師,有時在談到並行與非同步時也會感到困惑。
我們把一切都理順一下。開始吧!
簡短回顧 Task 出現的歷史
在過去那段美好的時光(在 .span class="code text-user">.NET 4.0 之前),執行平行或「背景」程式碼的唯一明顯方法就是建立新的 thread。例如,new Thread(() => { ... }).Start(); Threads 的好處是簡單。但它們的壞處是所有東西都壓在你肩上。資源分配、生命週期、例外處理、同步、監控、可擴展性 — 這些都需要開發者來處理。而作為程式設計師,我們多半喜歡更多的懶惰(較少的樣板和樣態)。
隨著 tasks(Task)的到來,一切都改變了 — 它們來自命名空間 System.Threading.Tasks.Task。Task 不是 thread。它是一個更抽象、更靈活的概念,描述了「某個時間會被完成的工作」,可能是並行執行的。
2. Thread —「裸露的 thread」
Thread 是低階的執行單位,代表作業系統分配的一塊資源(自己的 stack、執行上下文等)。如果你手動建立 thread,就要負責它的啟動、結束以及整個生命週期的所有細節。
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread thread = new Thread(() => {
Console.WriteLine("Hello from thread!");
});
thread.Start();
thread.Join(); // 等待 thread 結束
}
}
- 這裡我們建立了一個 thread,該 thread 在自己的 stack 上執行 lambda。
- 啟動後我們呼叫 Join(),以等待它完成。
問題在哪裡?
- 每個 thread 都佔用記憶體(stack,大約 1 MB)。
- 在 .NET 中不建議手動建立數千個 threads — 系統會很痛苦。
- 如果忘了呼叫 Join(),主執行緒可能會比子執行緒先結束,程式會「被切斷」。
- thread 內的例外不會自動冒泡出來 — 必須專門捕獲!
- 如果啟動了 thread,沒辦法「漂亮地」取消它(沒有 Stop()() 之類的方法)。
3. Task —「新世代的任務」
Task 是更智慧的抽象,代表「將來會完成的工作」。在底層,tasks 通常在 ThreadPool 的執行緒池上執行,這比無限制地建立大量 threads 更有效率。你不需要手動管理它們的建立,ThreadPool 會根據負載自動調整執行緒數量。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Task task = Task.Run(() =>
{
Console.WriteLine("Hello from Task!");
});
await task; // 等待 task 完成
}
}
- 這裡 task 不保證一定在獨立 thread 上運行,但通常會在執行緒池中的一個 thread 上執行。
- 你可以用熟悉的方式等待 task 完成(在非同步方法中用 await 或在同步中用 task.Wait())。
4. Task 與 Thread 的區別是什麼?
讓我們把差異整理清楚,說明各自適用的場景與一些不那麼明顯的陷阱。
| Thread | Task | |
|---|---|---|
| 抽象層級 | 作業系統的執行緒 | 工作/任務(抽象,可能使用執行緒) |
| 啟動方式 | 透過 new Thread(...).Start() | 透過 Task.Run(...)、Task.Factory.StartNew(...)、async 方法 |
| 直接控制 | 可以(啟動、Join、優先順序等) | 不可以,.NET 接管管理 |
| 執行緒池 | 沒有,thread 總是新建立的 | 有,通常使用 ThreadPool |
| 資源管理 | 分配專屬 stack | 資源由池重複使用 |
| 可擴展性 | 差:對於 1000+ threads 非常低效 | 優:成千上萬的 tasks 是合理的 |
| 互動性 | 從 OS 角度看是獨立的執行緒 | 可以是當前執行緒的延續,也可能在 ThreadPool 上 |
| 例外處理 | 需要明確捕捉,否則可能「消失」 | 例外會保存在 Task 中;可以在 await 或 .Wait() 時捕獲 |
| 取消 | 沒有標準方式 | 有,透過 CancellationToken 支援 |
| 等待結果 | 用 Join() 等待 | await、.Wait()、.Result |
| 適用於 | 特殊情況 — UI 專用 thread、長期存在的 threads | 幾乎所有背景/並行工作 |
5. 何時使用哪一個?
何時使用 Thread?
老實說,在現代 .NET 程式中很少需要手動建立 threads。以下是一些合理的例子:
- 需要建立一個非常長期運行的 thread(例如,將信號廣播到無線電或處理硬體資料),而且它是「特殊」的:需要較低優先順序、獨立的執行文化、獨立的名稱。
- 有時為了整合某些需要手動管理 thread 的低階 API。
- 在非常特殊的情況,例如自訂的任務排程器(custom schedulers)。
其他情況下 — Task 通常是更正確、更現代的選擇。
何時使用 Task?
幾乎所有需要在「背景」或「並行」執行工作的場合:
- 任何可以放到執行緒池跑的背景計算(例如,伺服器請求處理、檔案解析、發送郵件)。
- 啟動非同步操作(async/await)— 機制回傳 Task 或 Task<T>。
- 組合 tasks、處理續作(continuations)、建立任務鏈。
- 方便的取消、等待與收集結果:Task 支援 CancellationToken,並且容易整合到現代 API。
- 非同步 I/O 操作:網路請求、檔案操作、資料庫存取。
比較
| 情境 | Thread | Task |
|---|---|---|
| 長期存在的 thread(例如,自行管理的服務) | 是 | 否 |
| 大量短小任務的執行 | 否 | 是 |
| 非同步 I/O 操作(使用 await) | 否 | 是 |
| 任務組合、取消、任務鏈 | 否 | 是 |
| 需要精細設定優先順序與文化 | 是(但很少見) | 否,僅限預設任務行為 |
| 簡單地把工作分配到 CPU 核心 | 有時候 | 是 |
6. 有用的細節
Task — 不一定就是一個 thread!
最強大的魔法是:如果你用 Task 來做非同步 I/O,通常根本不會創建新的 thread!一切「魔法地」交給 I/O Completion Ports 或其他平台原語處理。在等待外部資源(檔案、網路、資料庫)時,thread 會被釋放;在等待期間沒有任何 thread 被佔用。
Task 與非同步(I/O-bound)— await 的魔力
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
// 非同步下載網站內容(I/O-bound)
HttpClient client = new HttpClient();
string data = await client.GetStringAsync("https://www.dotnetfoundation.org");
Console.WriteLine($"獲取的字元數:{data.Length}");
}
}
- 這裡的任務(Task<string>)封裝了一個非同步 I/O 操作。
- thread 不會被阻塞 — 它會繼續做別的事,當下載完成後方法會繼續執行。
- 為這種情況手動建立 thread 完全是多餘且低效率的做法。
Task 與 ThreadPool
當你呼叫 Task.Run(...) 或使用非同步 API(await)時,.NET 通常會使用一個特殊的執行緒池 — ThreadPool。這是一組事先建立好的 threads,它們「坐在替補席」上,能迅速接手任何進來的工作。工作少時 threads 休息,工作多時會自動增加新的 threads,但會有節制。因而你的應用能夠以任務數量做擴展,而不會對系統造成過度負擔。
透過 new Thread 建立的 thread 幾乎總是系統中的獨立「居民」— 它在結束後不會回到池中,而是直接死亡。這也是為什麼 Task 在大量並行場景中更有效率的原因。
7. 常見錯誤與陷阱
如果你一時興起想當復古程式設計師,全部用 threads 寫程式,那會有很多有趣的「冒險」:記憶體洩漏、複雜的同步、無法取消的工作、掛起的「幽靈」thread(殭屍進程)、以及需要透過專門的 API 才能捕獲與處理錯誤。
最重要的要記住的是:「Task」是便利、安全且現代化的。在大多數 C# 開發場景中,今天沒有理由回到手動管理 threads。
GO TO FULL VERSION