1. Task.WhenAll:等待全部完成
現實生活中很少只做一件事。你起床的時候會看錶、洗澡哼歌、穿衣服,同時準備咖啡。或是在電腦上工作時,同時上傳多個檔案、發送一堆網路請求、計算不同方向的一堆資料。這些動作都可以(而且應該)並行處理,才能不浪費使用者的時間。我們不想一個接一個慢慢等!要怎麼把這個「管弦樂」安排好?怎麼知道什麼時候全部完成?或者反過來,想知道誰最先完成?
Task.WhenAll 和 Task.WhenAny 就是為這些情境準備的工具。
方法 WhenAll
Task.WhenAll 是 Task 類別的靜態方法,它接受一個任務集合,並回傳一個新的任務,只有在所有傳入的任務都完成時才會完成。可以把它想像成在考試時你要等所有同學交卷才一起離開教室。
方法簽名
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/Task<T[]>) | 等待所有回應或所有檔案,聚合結果 |
|
當任一任務完成時 | 最先完成的那個任務(Task) | 使用第一個結果,取消其他任務 |
3. 結合 WhenAll 和 WhenAny — 真實情境
有時候需要先等待任一任務完成,再等所有任務;或反過來也會發生。
範例:「乒乓」在兩個伺服器間選最快
想像你同時向兩個伺服器(克隆)發送請求,以防其中一個變慢。但你只使用第一個回來的結果。
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.WhenAll 和 Task.WhenAny 的常見錯誤
錯誤 №1:以為 WhenAll 在第一個任務完成時就會結束。
新手常以為 Task.WhenAll 會在第一個任務完成時結束。實際上它會等所有任務完成。
錯誤 №2:忽略 WhenAll 裡的例外。
如果其中一個任務丟出例外,最終任務會處於 Faulted。不檢查 AggregateException.InnerExceptions 可能會漏掉重要錯誤。
錯誤 №3:在 WhenAny 中直接存取 Result 而不先檢查。
如果第一個完成的任務失敗了,存取 Result 會拋出例外。存取前應先檢查 Task.IsFaulted。
錯誤 №4:把任務順序執行而不是並行。
例如在迴圈裡逐個用 await 等待,而不是使用 Task.WhenAll,會嚴重降低效能。
GO TO FULL VERSION