如何用Task.WhenAll并行化.NET中的Http请求任务
并行处理批量Http请求的实现方案
一、核心实现:用Task.WhenAll收集结果再关联
不用在异步任务里操作ConcurrentDictionary,而是让每个任务返回item与对应结果的元组,等所有任务完成后一次性构建字典,既避免并发写入的锁开销,又能精准关联结果:
// 1. 获取初始items列表 var items = await FetchItemsViaHttpAsync(); // 2. 为每个item创建异步任务,返回(item, 处理结果)的元组 var processingTasks = items.Select(async item => { var itemResult = await FetchItemDetailViaHttpAsync(item); return (Item: item, Result: itemResult); }); // 3. 等待所有任务完成,拿到所有结果 var allResults = await Task.WhenAll(processingTasks); // 4. 直接将结果转成ConcurrentDictionary,初始化时指定预期长度 var resultDictionary = new ConcurrentDictionary<ItemType, ResultType>( allResults.ToDictionary(kv => kv.Item, kv => kv.Result) );
二、ConcurrentDictionary初始化优化
初始化时的预期长度直接用items的总数,因为最终字典里的键值对数量和items数量完全一致,和并行任务的线程数无关。线程数由.NET的任务调度器自动管理,无需手动指定。如果需要手动控制并发请求数(比如规避API限流),可以用SemaphoreSlim:
// 限制同时最多10个请求 var requestSemaphore = new SemaphoreSlim(10); var processingTasks = items.Select(async item => { await requestSemaphore.WaitAsync(); try { var itemResult = await FetchItemDetailViaHttpAsync(item); return (Item: item, Result: itemResult); } finally { requestSemaphore.Release(); } });
如果一定要指定ConcurrentDictionary的并发级别(影响内部锁的数量),可以传入Environment.ProcessorCount(和CPU核心数匹配),结合预期长度的完整初始化代码:
var resultDictionary = new ConcurrentDictionary<ItemType, ResultType>( concurrencyLevel: Environment.ProcessorCount, capacity: items.Count );
三、关键注意点
- 避免在异步任务内直接写入
ConcurrentDictionary:虽然线程安全,但会带来额外的锁竞争开销,不如先收集所有结果再批量构建高效。 - 并行度不是越高越好:如果目标API有限流策略,必须用信号量控制并发数,否则容易触发429等限流错误。
内容的提问来源于stack exchange,提问作者Zenith
相关产品推荐
相关产品推荐

