CodeGym /課程 /C# SELF /管理多個任務

管理多個任務

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

1. Task.WhenAll:等待全部完成

現實生活中很少只做一件事。你起床的時候會看錶、洗澡哼歌、穿衣服,同時準備咖啡。或是在電腦上工作時,同時上傳多個檔案、發送一堆網路請求、計算不同方向的一堆資料。這些動作都可以(而且應該)並行處理,才能不浪費使用者的時間。我們不想一個接一個慢慢等!要怎麼把這個「管弦樂」安排好?怎麼知道什麼時候全部完成?或者反過來,想知道誰最先完成?

Task.WhenAllTask.WhenAny 就是為這些情境準備的工具。

方法 WhenAll

Task.WhenAllTask 類別的靜態方法,它接受一個任務集合,並回傳一個新的任務,只有在所有傳入的任務都完成時才會完成。可以把它想像成在考試時你要等所有同學交卷才一起離開教室。

方法簽名

Task Task.WhenAll(params Task[] tasks)
Task<TResult[]> Task.WhenAll<TResult>(params Task<TResult>[] tasks)

有回傳結果的版本(Task<TResult>)和「空」的版本(Task)。

最簡單的例子:等待所有檔案下載

我們來同時下載三個檔案。為簡單起見用延遲模擬下載(Task.Delay)。範例是真實可跑的程式碼。

using System;
using System.Threading.Tasks;

class Program
{
    // 模擬檔案下載延遲,返回檔名
    static async Task<string> DownloadFileAsync(string fileName)
    {
        Console.WriteLine($"► 開始下載:{fileName}");
        await Task.Delay(2000); // 等待 2 秒
        Console.WriteLine($"✓ 下載完成:{fileName}");
        return fileName;
    }

    static async Task Main()
    {
        var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };

        // 同時啟動所有下載
        var downloadTasks = new Task<string>[files.Length];
        for (int i = 0; i < files.Length; i++)
        {
            downloadTasks[i] = DownloadFileAsync(files[i]);
        }

        // 等待所有下載完成
        string[] results = await Task.WhenAll(downloadTasks);

        Console.WriteLine($"所有下載完成!清單:{string.Join(", ", results)}");
    }
}

這裡發生了什麼?

  • 我們沒有一個一個依序等待。三個任務同時啟動(非同步)。
  • Task.WhenAll(downloadTasks) 回傳一個任務,只有當所有任務都完成時這個任務才會完成。
  • 之後可以使用所有任務的結果——顯示、處理或傳給別的地方。

類比

就像你交代三個快遞員各送一個包裹,而你想在通知老闆前等所有人都送達成功。

Task.WhenAll 的運作示意

sequenceDiagram
    participant Program
    participant 任務1
    participant 任務2
    participant 任務3

    Program->>任務1: 啟動
    Program->>任務2: 啟動
    Program->>任務3: 啟動
    任務1-->>Program: 完成(可能比其他早)
    任務2-->>Program: 完成
    任務3-->>Program: 完成
    Program->>Program: Task.WhenAll 完成,所有結果可用

如果其中一個任務以錯誤結束怎麼辦?

Task.WhenAll 不會在某個任務發生錯誤時立即終止。它會等待所有任務結束。如果至少有一個任務發生例外——最終的任務會處於 Faulted 狀態,並且會包含發生的所有例外的集合。

範例

static async Task<string> MayThrowAsync(string fileName)
{
    await Task.Delay(500);
    if (fileName == "fileB.txt")
        throw new Exception("下載 fileB.txt 發生錯誤");
    return fileName;
}

static async Task Main()
{
    var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };
    var downloadTasks = files.Select(MayThrowAsync).ToArray();
    try
    {
        string[] results = await Task.WhenAll(downloadTasks);
        Console.WriteLine($"一切正常:{string.Join(", ", results)}");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"至少有一個任務以錯誤結束:{ex.Message}");
    }
}

如果發生例外,想檢視所有內部錯誤可以使用 AggregateException.InnerExceptions
官方文件: Task.WhenAll docs

2. Task.WhenAny:等待第一個完成的任務

Task.WhenAny 也是靜態方法,但它會在任一傳入任務進入「完成」狀態時就完成(不論是成功還是失敗)。它回傳第一個完成的任務的參考。

簽名

Task<Task> Task.WhenAny(params Task[] tasks)
Task<Task<TResult>> Task.WhenAny<TResult>(params Task<TResult>[] tasks)

類比

誰先做完蛋糕誰就是了,其他人可以請他停止。有時我們只需要知道哪個伺服器最先回應,然後就用它的結果。

範例:誰比較快?

using System;
using System.Threading.Tasks;

class Program
{
    // 模擬不同速度的請求
    static async Task<string> RequestAsync(string name, int delay)
    {
        await Task.Delay(delay);
        return $"{name} 在 {delay} 毫秒內完成";
    }

    static async Task Main()
    {
        var taskA = RequestAsync("A", 1000);
        var taskB = RequestAsync("B", 700);    // 最快
        var taskC = RequestAsync("C", 1500);

        // 等待第一個完成的任務
        Task<string> finished = await Task.WhenAny(taskA, taskB, taskC);

        Console.WriteLine($"最先完成的是:{finished.Result}");
    }
}

WhenAny 我們可以知道哪個任務先到達終點。之後可以取消其他任務,例如用 CancellationToken(下一堂會講)。

為什麼 WhenAny 回傳的是任務而不是直接的結果?

因為方法不知道哪個任務會先完成:它無法事先知道任務的 result 型別或是哪一個任務。因此它回傳整個任務——之後我們自己從該任務取結果:用 await 或透過該任務的 Result 屬性。

WhenAll vs WhenAny:比較示意

方法 何時觸發 回傳什麼 典型情境
Task.WhenAll
全部 完成時 任務(Task/Task<T[]> 等待所有回應或所有檔案,聚合結果
Task.WhenAny
當任一任務完成時 最先完成的那個任務(Task) 使用第一個結果,取消其他任務

3. 結合 WhenAllWhenAny — 真實情境

有時候需要先等待任一任務完成,再等所有任務;或反過來也會發生。

範例:「乒乓」在兩個伺服器間選最快

想像你同時向兩個伺服器(克隆)發送請求,以防其中一個變慢。但你只使用第一個回來的結果。

static async Task Main()
{
    var fastServer = RequestAsync("快速伺服器", 400);   // 400 ms
    var slowServer = RequestAsync("慢速伺服器", 2000); // 2000 ms

    var completed = await Task.WhenAny(fastServer, slowServer);
    Console.WriteLine(await completed);

    // *可選*:在這裡可以用 CancellationToken 取消還沒完成的任務
}

範例:只有在所有資料載入完才開始處理

你的應用有三個資料來源。只有當三個都載入完成,才開始處理:

static async Task Main()
{
    var task1 = DownloadFileAsync("a.txt");
    var task2 = DownloadFileAsync("b.txt");
    var task3 = DownloadFileAsync("c.txt");

    var all = await Task.WhenAll(task1, task2, task3);

    ProcessFiles(all[0], all[1], all[2]);
}

4. 使用 Task.WhenAllTask.WhenAny 的常見錯誤

錯誤 №1:以為 WhenAll 在第一個任務完成時就會結束。
新手常以為 Task.WhenAll 會在第一個任務完成時結束。實際上它會等所有任務完成。

錯誤 №2:忽略 WhenAll 裡的例外。
如果其中一個任務丟出例外,最終任務會處於 Faulted。不檢查 AggregateException.InnerExceptions 可能會漏掉重要錯誤。

錯誤 №3:在 WhenAny 中直接存取 Result 而不先檢查。
如果第一個完成的任務失敗了,存取 Result 會拋出例外。存取前應先檢查 Task.IsFaulted

錯誤 №4:把任務順序執行而不是並行。
例如在迴圈裡逐個用 await 等待,而不是使用 Task.WhenAll,會嚴重降低效能。

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