明明是一段簡單的邏輯運算,既沒有訪問資料庫,也沒有進行任何外部資源的 I/O 操作
但執行時間卻莫名其妙延遲到 2~5 秒,深入檢查程式碼後,發現問題出在 Task.WhenAll 和 AsParallel 的混用上
這兩者分別適用於不同場景,但當它們一起使用時,會導致不必要的上下文切換和資源競爭,進而大幅降低程式效能
這樣的誤用雖然容易被忽視,但卻是導致效能問題的常見原因
接下來,說明Task.WhenAll 和 AsParallel 的特性、誤用的具體表現,並提供正確的改進方式來避免這些問題
誤用導致的問題
重複平行化
AsParallel 已經讓操作在多核心上平行執行,但每個操作又包裝為非同步任務並由 Task.WhenAll 處理,這樣的重複平行化會增加額外的開銷
資源競爭
AsParallel 依賴多核心 CPU,Task.WhenAll 使用線程池管理任務。如果同時使用,可能造成多核執行和線程池爭奪資源
上下文切換開銷
每個 Task 的創建與調度都需要上下文切換。當 AsParallel 平行處理中每個任務都包裝為 Task 時,將大幅增加上下文切換的成本。
不可控的併發數
AsParallel 的平行度是基於系統核心數,而 Task.WhenAll 是基於任務數量,這可能導致超過系統負荷的併發數
錯誤範例:平行化非同步操作
var results = await Task.WhenAll( data.AsParallel().Select(async item => { await Task.Delay(100); // 模擬非同步操作 return item * 2; }) );
AsParallel 已將 data 分片到多核處理,但每個操作都生成了一個非同步任務,交由 Task.WhenAll 調度,造成上下文切換的成本
對於非同步任務,應該直接用 Task.WhenAll
錯誤範例:重複的平行度控制
var results = await Task.WhenAll( data.AsParallel().WithDegreeOfParallelism(4).Select(async item => { await Task.Delay(100); // 模擬非同步操作 return item * 2; }) );
AsParallel().WithDegreeOfParallelism(4) 限制了同時處理的核心數,但 Task.WhenAll 仍會嘗試啟動所有任務
這種組合會導致非同步任務和多核調度的混亂執行
正確範例:處理同步操作
var results = data.AsParallel() .Select(item => { // 模擬同步操作 Thread.Sleep(100); // 假設是 CPU 密集型操作 return item * 2; }) .ToList();
如果是 CPU 密集型的同步操作,只需要使用 AsParallel 利用多核處理同步任務,效率更高,無需非同步
正確範例 :處理非同步操作
var tasks = data.Select(async item => { await Task.Delay(100); // 模擬非同步操作 return item * 2; }); var results = await Task.WhenAll(tasks);
如果是 I/O 密集型的非同步操作,只需要使用 Task.WhenAll
正確範例 :限制非同步的併發數
var semaphore = new SemaphoreSlim(4); // 最大併發數 4 var tasks = data.Select(async item => { await semaphore.WaitAsync(); try { await Task.Delay(100); // 模擬非同步操作 return item * 2; } finally { semaphore.Release(); } }); var results = await Task.WhenAll(tasks);
當需要控制非同步任務的最大併發數時,可以使用 SemaphoreSlim 限制同時執行的非同步任務數量,避免系統過載
結論
- AsParallel 適用於同步、CPU 密集型任務
- Task.WhenAll 適用於非同步、I/O 密集型任務
- 不要混用 AsParallel 和 Task.WhenAll,避免資源競爭和上下文切換成本
- 控制併發數:對於大量非同步操作,可以使用 SemaphoreSlim 限制併發