CodeGym /課程 /C# SELF /比較 Task

比較 TaskThread

C# SELF
等級 60 , 課堂 1
開放

1. 介紹

現在是時候弄清楚 — TaskThread 本質上有什麼不同?為什麼 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. TaskThread 的區別是什麼?

讓我們把差異整理清楚,說明各自適用的場景與一些不那麼明顯的陷阱。

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)— 機制回傳 TaskTask<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 完全是多餘且低效率的做法。

TaskThreadPool

當你呼叫 Task.Run(...) 或使用非同步 API(await)時,.NET 通常會使用一個特殊的執行緒池 — ThreadPool。這是一組事先建立好的 threads,它們「坐在替補席」上,能迅速接手任何進來的工作。工作少時 threads 休息,工作多時會自動增加新的 threads,但會有節制。因而你的應用能夠以任務數量做擴展,而不會對系統造成過度負擔。

透過 new Thread 建立的 thread 幾乎總是系統中的獨立「居民」— 它在結束後不會回到池中,而是直接死亡。這也是為什麼 Task 在大量並行場景中更有效率的原因。

7. 常見錯誤與陷阱

如果你一時興起想當復古程式設計師,全部用 threads 寫程式,那會有很多有趣的「冒險」:記憶體洩漏、複雜的同步、無法取消的工作、掛起的「幽靈」thread(殭屍進程)、以及需要透過專門的 API 才能捕獲與處理錯誤。

最重要的要記住的是:「Task」是便利、安全且現代化的。在大多數 C# 開發場景中,今天沒有理由回到手動管理 threads。

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