CodeGym /課程 /C# SELF /類別 Task 與

類別 Task 與 Task<TResult>

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

1. 介紹

在非同步程式設計的世界,我們遵循「委派並回報」的原則。想像一下:你要下載一個超大的檔案、分析幾 GB 的日誌或向遠端伺服器發出請求。與其像雕像一樣呆在那兒等,不如跟系統說:「去處理這件事吧,我先做別的。完成後一定通知我!」

這時候 Task 出場了 — 它是這種做法的優雅化身。這不只是技術抽象,而是一種「聰明的中介」,負責執行工作並確保結果不會消失在數位世界裡。

Task 就像你交代事情給私人助理。他點頭、把任務記在本子上,說:「你去做你的事吧,我會在完成時回報。」而且真的會回報——要麼帶著結果,要麼誠實說明發生了什麼問題。

用生活化的比喻,Task 很像線上預約系統:你線上掛號、收到確認,無需在現場苦等。系統會在該到時候提醒你,你則可以繼續做別的事。

類別 Task

Task 是 .NET 非同步程式設計的基礎積木。它代表一個正在執行或將在未來完成的操作,結果會在將來可用。如果方法不需要回傳值,就用單純的 Task。

public async Task BackupToCloudAsync()
{
    // 做備份的 magic,不回傳任何東西
}

類別 Task<TResult>

如果需要回傳結果(例如 string、number、object…),就用 Task<TResult>:

public async Task<string> DownloadHtmlAsync(string url)
{
    // 下載頁面並回傳 HTML
    return "<html>...</html>";
}

為什麼用 Task 而不是 Thread?

Thread 管理實際的執行緒(這比較重且危險),而 Task 是更高層的抽象:它可能在 thread pool 執行、也能在不建立新執行緒的情況下作非同步 I/O,開發者不用操心低層細節。

Task 幫你描述「我要執行這個動作」,至於如何執行—就交給 .NET 去決定。

2. Task 物件的構成

Task 必須知道的屬性與方法

屬性 / 方法 說明
Status
任務的當前狀態
Result
Task<TResult> 的結果(會阻塞執行緒)
IsCompleted
任務是否已完成
IsFaulted
任務是否發生例外
IsCanceled
任務是否已被取消
Wait()
阻塞目前執行緒直到完成(危險)
ContinueWith()
在完成後啟動另一個任務
Exception
若 Task 因錯誤結束,可取得該例外
Id
任務的唯一識別碼

非同步方法與 Task 的運作方式

sequenceDiagram
    participant Main as Main 執行緒
    participant Task as Task (背景任務)
    Main->>Task: 啟動 Task.Run(() => ...)
    Note right of Task: 在背景執行
(CPU 或 I/O) alt 任務完成 Task->>Main: await 完成,繼續執行 else 錯誤 Task->>Main: await 拋出例外 end

3. 建立與啟動任務:Task 如何工作

帶 async 的非同步方法

最常見的情況是把方法宣告為 async,回傳 Task 或 Task<TResult>(如上所述)。

Task.Run:在 thread pool 執行

若你要在背景做費時的工作(例如大量計算或編碼影片),可以用 Task.Run:

Task work = Task.Run(() =>
{
    // 重度運算 — 不要阻塞主執行緒!
    Console.WriteLine("背景計算開始...");
    Thread.Sleep(2000); // 模擬耗時工作
    Console.WriteLine("背景計算完成!");
});

如果需要取得結果:

Task<int> calculateTask = Task.Run(() =>
{
    // 例如計算前 100 個數字的總和
    int sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    return sum;
});

Task.Factory.StartNew

這是比較低層且可調整的方式,允許指定 scheduler、傳遞參數等。在現代程式中通常建議用 Task.Run,它更簡單且減少錯誤。

4. 今日範例應用:我們的圖書目錄

假設有一個圖書目錄應用,我們要新增從「雲端」載入書籍的功能 — 這是一個 I/O-bound 操作(慢的 HTTP 請求或檔案讀取)。

新增一個非同步「載入」書籍的方法(用延遲模擬):

public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
}

public class BookCatalog
{
    public List<Book> Books { get; set; } = new();

    public async Task LoadBooksAsync()
    {
        Console.WriteLine("載入書籍...");
        await Task.Delay(2000); // 模擬較慢的載入(例如 HTTP 或 檔案)
        Books = new List<Book>
        {
            new Book { Title = "CLR via C#", Author = "Jeffrey Richter" },
            new Book { Title = "C# in Depth", Author = "Jon Skeet" }
        };
        Console.WriteLine("書籍已成功載入。");
    }
}

在 Main 呼叫非同步載入(用 await):

var catalog = new BookCatalog();
await catalog.LoadBooksAsync();
Console.WriteLine($"目錄中有 {catalog.Books.Count} 本書。");

表格:建立與啟動 Task 的主要方式

建立方法 如何使用 結果 適用情境
async 方法
async Task / async Task<T>
非同步操作 通常用於 I/O,較方便
Task.Run
Task.Run(() => { ... })
背景任務 CPU-bound(計算)
TaskCompletionSource<T>
手動建立並完成 Task 完全由程式設計師掌控 少見,適用低階情境

5. Task 的生命週期

Task 可以處於多種狀態:

  • Created — 任務已建立但尚未啟動(適用於需明確啟動的 Task)。
  • WaitingToRun — 在 thread pool 等待排程。
  • Running — 正在執行。
  • WaitingForActivation — 等待啟動或外部啟動。
  • RanToCompletion — 成功完成。
  • Faulted — 因例外而結束。
  • Canceled — 被取消(若支援取消)。

流程圖

flowchart LR
    Start -->|啟動任務| Running
    Running -->|成功| Completed
    Running -->|錯誤| Faulted
    Running -->|取消| Canceled

實作檢驗

Task task = Task.Run(() =>
{
    Thread.Sleep(1000);
});
Console.WriteLine(task.Status); // 通常:Running 或 WaitingToRun
await task;
Console.WriteLine(task.Status); // 完成後為 RanToCompletion

6. 如何從 Task<TResult> 取得結果?

Task<TResult> 包裹一個將在未來出現的結果。需要等待結果時,用 await:

Task<int> sumTask = Task.Run(() =>
{
    int sum = 0;
    for (int i = 1; i <= 5; i++) sum += i;
    return sum;
});

int result = await sumTask;
Console.WriteLine(result); // 15

如果忘了寫 await,你會得到一個 Task(一個承諾),而不是結果。這是常見的「非同步陷阱」。

替代:同步取得結果(不要在 UI 使用!)

有時(例如在測試)你可能想在不 await 的情況下拿到結果,可以用屬性 .Result:

int result = sumTask.Result;

但若 Task 尚未完成,這會 阻塞 執行緒;若在 UI 執行緒使用,應用會凍結!所以:盡量使用 await。

與 Task / Task<TResult> 有關的常見錯誤

忘記回傳 Task,方法變成 void。若方法沒有回傳值 — 要回傳 Task,不要用 void,否則無法處理錯誤。

忽略 await。只是呼叫方法但不等待,任務就會自行「fire and forget」。你無法知道它何時完成或是否失敗。

透過 .Result 或 .Wait() 進行阻塞等待。很容易導致 deadlock,尤其在 UI 或 ASP.NET。請只用 await。

7. 進階功能

任務鏈:ContinueWith

可以在任務完成後接續執行動作,使用 ContinueWith:

Task.Run(() => 10)
    .ContinueWith(t =>
    {
        Console.WriteLine($"完成!結果: {t.Result}");
    });

但在現代 C# 中通常用 async/await,可讀性更好。

範例:並行與序列式載入資料

假設要從兩個不同來源載入兩本書。可以同時啟動兩個 Task,再等待兩者完成:

public async Task LoadBooksFromMultipleSourcesAsync()
{
    Task<List<Book>> t1 = LoadFromCloudAsync();
    Task<List<Book>> t2 = LoadFromLocalAsync();

    // 並行等待兩個任務
    await Task.WhenAll(t1, t2);

    // 合併結果
    Books = t1.Result.Concat(t2.Result).ToList();
}

private async Task<List<Book>> LoadFromCloudAsync()
{
    await Task.Delay(2000); // "雲端"
    return new List<Book> { new Book { Title = "Cloud Book", Author = "Cloud Author" } };
}

private async Task<List<Book>> LoadFromLocalAsync()
{
    await Task.Delay(1000); // "本機磁碟"
    return new List<Book> { new Book { Title = "Local Book", Author = "Local Author" } };
}

注意:使用 await 搭配 Task.WhenAll(...) 可以讓兩個請求同時啟動並並行執行(若環境允許),應用會等到兩者都完成。

8. 實用小技巧

Task 與 Fire-and-forget

有時你可能想啟動一個任務但不等待它(例如把日誌傳到雲端或在背後顯示 toast):

async void LogToCloudAsync(string message)
{
    await Task.Run(() =>
    {
        // 長時間傳送日誌
        Thread.Sleep(1000);
        Console.WriteLine($"日誌已傳送: {message}");
    });
}

但要記住:若此類任務發生錯誤,會很難偵測到。若可能,還是回傳 Task,並至少在內部記錄例外!

Task 與 Task<TResult> 在實務中的應用

  • 在 UWP/WPF/WinForms 客戶端應用不要阻塞 UI — 使用 Task 處理長時間操作(檔案、網路)。
  • 在 WebAPI/ASP.NET 中 Task 可以避免浪費執行緒在等待網路/DB,提升效能。
  • 安排「平行」執行:同時下載、處理、儲存。
  • 幾乎所有耗時方法都有 Async 版本:File.ReadAllTextAsync、HttpClient.GetStringAsync 等。

常見問與答與意外情況

問題:為什麼 Task 有時同步完成?
回答:若操作已完成(例如結果被快取),compiler 或 scheduler 可能會在相同執行緒同步完成方法。這是正常的,能加速重複呼叫。

問題:為什麼不能用 async void?
回答:這種方法沒辦法等待,也無法捕捉錯誤或追蹤完成狀態。請使用 Task;async void 只應用於 EventHandler(例如 Button_Click)。

問題:可以啟動多個任務但只等待其中一個嗎?
回答:可以 — 使用 Task.WhenAny。

2
任務
C# SELF, 等級 60, 課堂 0
上鎖
使用 Task 來回傳結果
使用 Task 來回傳結果
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION