.NET Task.WhenAll 與 AsParallel() 誤用效能瓶頸

明明是一段簡單的邏輯運算,既沒有訪問資料庫,也沒有進行任何外部資源的 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 限制併發
訂閱
通知
guest
0 留言
預約回饋
查看所有留言