在C#的异步编程体系中,Parallel.ForEachAsync和Task.WhenAll都是处理并发异步任务的常用工具,但两者的设计目标和执行逻辑存在本质差异,适用场景也各不相同。

Parallel.ForEachAsync 的基本用法
Parallel.ForEachAsync是.NET 6及以上版本引入的API,专门用于并行执行异步操作,它会自动控制并发度,避免同时启动过多异步任务导致资源耗尽。它的核心作用是遍历一个集合,对集合中的每个元素执行异步操作,并且可以设置最大并行数。
下面是一个简单的使用示例,模拟对一组用户ID异步获取用户信息,最大并行数设置为3:
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
// 待处理的用户ID集合
List<int> userIds = new List<int> { 1, 2, 3, 4, 5, 6 };
// 配置并行选项,设置最大并行数为3
ParallelOptions options = new ParallelOptions
{
MaxDegreeOfParallelism = 3
};
// 使用Parallel.ForEachAsync并行处理每个用户ID
await Parallel.ForEachAsync(userIds, options, async (userId, cancellationToken) =>
{
await GetUserInfoAsync(userId, cancellationToken);
});
Console.WriteLine("所有用户信息处理完成");
}
// 模拟异步获取用户信息的操作
static async Task GetUserInfoAsync(int userId, CancellationToken cancellationToken)
{
Console.WriteLine($"开始处理用户ID:{userId},线程ID:{Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000, cancellationToken); // 模拟异步耗时操作
Console.WriteLine($"用户ID:{userId} 处理完成");
}
}
上述代码中,Parallel.ForEachAsync会按照设置的最大并行数3来调度任务,当某个任务完成后,会自动启动下一个任务,始终保持同时运行的任务数不超过3。
Task.WhenAll 的基本用法
Task.WhenAll的作用是将多个已经创建的异步任务组合起来,等待所有任务都完成后再继续执行后续逻辑。它不会主动控制任务的启动时机和并发数,所有任务会在调用Task.WhenAll之前就已经启动,或者由开发者手动控制启动逻辑。
下面是使用Task.WhenAll处理相同场景的示例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
List<int> userIds = new List<int> { 1, 2, 3, 4, 5, 6 };
// 先创建所有异步任务,此时所有任务会立即开始执行
List<Task> tasks = userIds.Select(userId => GetUserInfoAsync(userId, CancellationToken.None)).ToList();
// 等待所有任务完成
await Task.WhenAll(tasks);
Console.WriteLine("所有用户信息处理完成");
}
static async Task GetUserInfoAsync(int userId, CancellationToken cancellationToken)
{
Console.WriteLine($"开始处理用户ID:{userId},线程ID:{Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000, cancellationToken);
Console.WriteLine($"用户ID:{userId} 处理完成");
}
}
这个示例中,所有6个异步任务会在Select执行时全部启动,没有并发度限制,可能会同时运行6个任务。
两者的核心区别
1. 任务调度逻辑不同
Parallel.ForEachAsync是主动调度任务,它会根据设置的最大并行数,动态启动和调度任务,始终控制同时运行的任务数量不超过阈值。而Task.WhenAll是被动等待,它只负责等待已经创建的任务完成,不会干预任务的启动和并发数量,所有任务的启动时机由开发者控制。
2. 适用场景不同
如果需要处理一个集合中的元素,并且希望控制并发度,避免资源占用过高,优先选择Parallel.ForEachAsync。如果是已经有一组独立的异步任务,需要等待所有任务完成后再做后续处理,优先选择Task.WhenAll。
3. 异常处理方式不同
Parallel.ForEachAsync中如果某个任务抛出异常,会取消其他尚未完成的任务,并且将异常包装为AggregateException抛出。而Task.WhenAll中如果某个任务抛出异常,其他任务仍然会继续执行,所有任务的异常会汇总到返回的Task中,需要开发者手动捕获处理。
4. 资源控制能力不同
Parallel.ForEachAsync内置了并发度控制机制,通过ParallelOptions.MaxDegreeOfParallelism就可以轻松设置最大并行数,适合对资源敏感的场景。Task.WhenAll本身没有资源控制能力,如果需要控制并发度,需要开发者额外实现任务调度逻辑,比如使用信号量SemaphoreSlim来限制同时运行的任务数量。
如何选择
如果你的需求是遍历集合执行异步操作,并且需要控制并发数,直接使用Parallel.ForEachAsync即可,它已经内置了完善的调度逻辑。如果你已经有多个独立的异步任务,或者需要自定义任务的启动逻辑,那么使用Task.WhenAll更合适。如果需要在Task.WhenAll的场景下控制并发度,可以结合SemaphoreSlim实现,示例如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
List<int> userIds = new List<int> { 1, 2, 3, 4, 5, 6 };
// 创建信号量,限制最大并发数为3
SemaphoreSlim semaphore = new SemaphoreSlim(3);
List<Task> tasks = userIds.Select(async userId =>
{
// 等待信号量,获取执行权限
await semaphore.WaitAsync();
try
{
await GetUserInfoAsync(userId, CancellationToken.None);
}
finally
{
// 释放信号量,允许其他任务执行
semaphore.Release();
}
}).ToList();
await Task.WhenAll(tasks);
Console.WriteLine("所有用户信息处理完成");
}
static async Task GetUserInfoAsync(int userId, CancellationToken cancellationToken)
{
Console.WriteLine($"开始处理用户ID:{userId},线程ID:{Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000, cancellationToken);
Console.WriteLine($"用户ID:{userId} 处理完成");
}
}
这种组合方式可以实现和Parallel.ForEachAsync类似的并发控制效果,但代码复杂度更高,所以如果不需要自定义调度逻辑,优先选择Parallel.ForEachAsync。
Parallel.ForEachAsyncTask.WhenAllC#异步编程修改时间:2026-06-30 02:39:33