在現代 .NET 應用程序開發中,異步編程(Asynchronous Programming)已成為提升性能、改善響應能力和充分利用多核處理器的關鍵技術。async
和 await
關鍵字極大地簡化了異步代碼的編寫,而 Task
類則是這一模型的核心。在處理多個并發操作時,Task.WhenAll
方法是一個強大且常用的工具。本文將深入探討 Task.WhenAll
的工作原理、使用場景、最佳實踐以及需要注意的陷阱。
什么是 Task.WhenAll
?
Task.WhenAll
是 Task
類提供的一個靜態方法,用于等待多個任務(Task
或 Task<T>
)全部完成。它接收一個任務集合(如 IEnumerable<Task>
或 Task[]
),并返回一個新的 Task
或 Task<TResult[]>
。當傳入的任務全部成功完成時,返回的任務才會完成。如果其中任何一個任務因異常而失敗,返回的任務也會以異常狀態完成。
基本語法
// 等待多個無返回值的任務完成
Task WhenAll(params Task[] tasks);
Task WhenAll(IEnumerable<Task> tasks);// 等待多個有返回值的任務完成
Task<TResult[]> WhenAll<TResult>(params Task<TResult>[] tasks);
Task<TResult[]> WhenAll<TResult>(IEnumerable<Task<TResult>> tasks);
為什么需要 Task.WhenAll
?
在沒有 Task.WhenAll
的情況下,我們可能會這樣處理多個異步操作:
// ? 錯誤方式:順序執行,效率低下
var result1 = await GetDataAsync(url1);
var result2 = await GetDataAsync(url2);
var result3 = await GetDataAsync(url3);
// 所有操作是串行的,總耗時約等于各操作耗時之和
或者使用 Task.WhenAny
,但它只等待第一個完成的任務,無法滿足“全部完成”的需求。
Task.WhenAll
允許我們并發地啟動所有操作,然后等待它們全部結束,從而顯著減少總執行時間。
? 正確方式:使用 Task.WhenAll
// 啟動所有任務(并發)
var task1 = GetDataAsync(url1);
var task2 = GetDataAsync(url2);
var task3 = GetDataAsync(url3);// 等待所有任務完成
await Task.WhenAll(task1, task2, task3);// 所有任務都已完成,可以安全地獲取結果
var result1 = await task1; // 不會阻塞,因為任務已結束
var result2 = await task2;
var result3 = await task3;
或者更簡潔地:
var tasks = new[]
{GetDataAsync(url1),GetDataAsync(url2),GetDataAsync(url3)
};await Task.WhenAll(tasks);
// 結果按任務在數組中的順序排列
var results = await Task.WhenAll(tasks); // 對于有返回值的任務
核心優勢
- 性能提升:通過并發執行,總耗時通常接近于最慢的那個任務的耗時,而不是所有任務耗時之和。
- 代碼簡潔:避免了復雜的
Task.ContinueWith
鏈或手動管理多個TaskCompletionSource
。 - 異常處理集中:所有任務的異常都可以在
await Task.WhenAll(...)
處被捕獲(通常包裝在AggregateException
中)。 - 結果聚合:對于
Task<T>
,WhenAll
直接返回一個包含所有結果的數組,順序與輸入任務一致。
重要注意事項與最佳實踐
1. 任務必須已經啟動
Task.WhenAll
等待的是已經啟動的任務。確保你在調用 Task.WhenAll
之前已經啟動了所有任務(即調用了異步方法但沒有 await
它)。
// ? 正確:任務已啟動
var task1 = SomeAsyncOperation(); // 啟動任務
var task2 = AnotherAsyncOperation(); // 啟動任務
await Task.WhenAll(task1, task2);// ? 錯誤:任務未啟動(SomeAsyncOperation 返回的是 Task,但未執行)
// await Task.WhenAll(SomeAsyncOperation(), AnotherAsyncOperation());
// 這行代碼本身會啟動任務,但寫法不直觀,建議分開寫。
2. 異常處理
Task.WhenAll
返回的任務在任何一個輸入任務失敗時都會失敗。異常通常被包裝在 AggregateException
中。推薦使用 try-catch
塊來處理:
try
{await Task.WhenAll(task1, task2, task3);
}
catch (Exception ex)
{// ex 可能是 AggregateException 或單個異常(.NET 4.5+ 有時會扁平化)// 檢查 ex.InnerException 或 ex.Flatten().InnerExceptions 來獲取具體異常Console.WriteLine($"一個或多個任務失敗: {ex.Message}");// 可以遍歷所有任務檢查其狀態foreach (var task in new[] { task1, task2, task3 }){if (task.IsFaulted){Console.WriteLine($"任務 {task.Id} 失敗: {task.Exception?.InnerException?.Message}");}}
}
3. 性能考量:避免不必要的 Task.WhenAll
如果任務數量很少(比如1-2個),直接 await
每個任務可能更清晰。Task.WhenAll
在處理多個(例如3個以上)獨立任務時優勢最明顯。
4. 與 Task.WhenAny
的區別
Task.WhenAll
: 等待所有任務完成。Task.WhenAny
: 等待任何一個任務完成。適用于“競態”場景,比如從多個數據源獲取數據,取最快返回的結果。
5. 內存與資源管理
啟動大量并發任務可能會耗盡系統資源(如線程池線程、網絡連接、文件句柄)。考慮使用 SemaphoreSlim
或 Parallel.ForEachAsync
(C# 11+) 來限制并發度。
后續其他文章再展開講解(這里先挖個坑~~~~,可私信我填坑,我怕忘記了。)
6. 創建一個帶有默認值的已完成任務!
有的時候,在進入其他分支的時候,某個task可能是null,這樣Task.WhenAll等待的時候就會報錯,我們需要創建一個帶有默認值的已完成任務
.
比如:
Task task3 = Task.FromResult(default(bool))
- default(bool)
default 是 C# 中的一個關鍵字,用于獲取類型的默認值。
對于值類型 bool,其默認值是 false。
所以,default(bool) 等價于 false。 - Task.FromResult(T result)
這是 Task 類的一個靜態方法。
它接收一個類型為 T 的參數 result。
它返回一個 Task 對象,這個對象的狀態是已完成(RanToCompletion),并且其 Result 屬性的值就是傳入的 result。
這個方法非常高效,因為它不會創建一個新的線程或啟動任何異步工作;它只是包裝一個已經存在的值到一個 Task 的殼子里。 - Task.FromResult(default(bool))
將兩者結合起來,Task.FromResult(default(bool)) 創建并返回一個 Task。
這個 Task 立即完成。
當你 await 這個任務時,你會立即得到 false。
7 混合處理無返回值與有返回值的任務
當我們需要同時等待一個無返回值的 Task 和一個有返回值的 Task 時,Task.WhenAll 仍然適用,但獲取返回值該怎么做呢?
實現方式?
Task.WhenAll 可以接收混合類型的任務(Task 和 Task),但返回的是 Task 而非 Task<T[]>。因此,我們需要通過原始的有返回值任務的引用獲取結果。
using System;
using System.Threading.Tasks;class Program
{static async Task Main(string[] args){// 無返回值的任務Task task1 = Task.Run(() =>{Console.WriteLine("任務1(無返回值)開始");Task.Delay(2000).Wait();Console.WriteLine("任務1完成");});// 有返回值的任務(返回bool)Task<bool> task2 = Task.Run(() =>{Console.WriteLine("任務2(有返回值)開始");Task.Delay(1500).Wait();Console.WriteLine("任務2完成");return true; // 返回bool結果});// 同時等待兩個任務完成await Task.WhenAll(task1, task2);// 單獨獲取有返回值的任務結果bool result = await task2; // 更推薦的異步方式//bool result = task2.Result; 這么寫也是可以的!!!!!!Console.WriteLine($"任務2的返回值:{result}");Console.WriteLine("所有任務都已完成");}
}
最后給一個實際的例子
/// <summary>/// 采集圖片進行推理/// </summary>/// <param name="index">對應的相機:0 正面相機; 1 反面相機</param>/// <returns></returns>async Task<bool> RunOne(int index){Stopwatch 計時器 = new Stopwatch();List<YOLOData> data = new List<YOLOData>();CameraConfig info;GraphicInfo graphic;//YOLODetection yolo;HObject Hobj;try{info = GlobalData.Instance.saveInfo.NeedOpenedCameraList[index];graphic = GlobalData.Instance.saveInfo.Graphics.First(t=>t.SerialNumber == info.SerialNumber);//yolo = graphic.yolo;}catch (Exception){throw new Exception($"NeedOpenedCameraList或者graphic,未找到第{index}個相機");}Task task1, task2;Task<bool> task3 = Task.FromResult(default(bool));try{計時器.Restart();//----觸發運動(從初始位置到結束位置)await motionCard.PmoveEx(axisConfigInfo, axisConfigInfo.Positions[0].Value);logger.Info($"開始運動到{axisConfigInfo.Positions[0].Value}");//觸發一次采集!CameraService.Snap(info.SerialNumber);//----觸發運動(從結束位置到初始位置)task1 = motionCard.PmoveEx(axisConfigInfo, axisConfigInfo.Positions[1].Value);logger.Info($"結束運動到{axisConfigInfo.Positions[1].Value}");//采集圖片Hobj = CameraService.GetHImage(info.SerialNumber, info.Timeout);logger.Info($"采集到圖片!");計時器.Stop();//----觸發運動(從初始位置到結束位置)task2 = motionCard.PmoveEx(axisConfigInfo, axisConfigInfo.Positions[0].Value);logger.Info($"回到開始位置!{axisConfigInfo.Positions[0].Value}");}catch (Exception ex){MessageBox.Show($"運動采集失敗{ex.Message}");return false;}var t1 = 計時器.ElapsedMilliseconds;bool b = false;if (Hobj != null){task3 = ImgCheck(graphic, Hobj);}else{MainWindowViewModel.PostGrowlEvent(info.SerialNumber + "相機獲取圖片失敗", EnumAlarmType.Warning);Task.Delay(1000).Wait();b = false;}await Task.WhenAll(task1, task2, task3);//獲取結果b = task3.Result;logger.Info($"完成一次檢測~~~~~結果為:{b}");return b;}
總結
Task.WhenAll
是 .NET 異步編程工具箱中不可或缺的一部分。它通過并發執行多個獨立的異步操作,極大地提升了應用程序的效率和響應能力。正確理解和使用 Task.WhenAll
,能夠讓你的代碼更加高效、簡潔和健壯。
核心要點回顧:
- 并發啟動,然后等待全部完成。
- 顯著減少總執行時間。
- 集中處理異常和聚合結果。
- 注意任務啟動時機和異常處理。
- 根據場景控制并發度。
掌握 Task.WhenAll
,讓你的 .NET 應用在處理多任務時游刃有余!