C#异步任务未全部执行及字典重复键问题求助
问题分析与修复方案
核心问题原因
1. 任务未全部完成、字典条目不足
- HttpClient滥用:每个任务创建新
HttpClient实例,会导致TCP连接耗尽,部分请求因无法建立连接而失败,且未处理失败场景,失败请求不会写入字典。 - 未处理异常:请求过程中若发生网络错误、API限流等异常,任务会抛出未捕获的异常,虽
Task.WhenAll会等待任务完成,但异常会导致该任务的结果无法写入字典。 - 线程池校验冗余:手动等待线程池可用线程无意义,
Task.Run会自动调度线程池资源,该逻辑反而可能阻塞任务创建,导致部分任务未正确启动。
2. "添加已存在键"异常
Dictionary<TKey,TValue>并非线程安全集合,即便currentRequestId是唯一的,多个线程同时执行pi.Add时,会破坏集合内部哈希表结构,引发键重复的错误(实际并非真的重复,而是集合内部状态异常)。
修复后的代码
const int TotalRequests = 50; static int requestsCount = 0; static Dictionary<int, string> pi = new(); static readonly object lockObj = new(); // 复用全局HttpClient实例 static readonly HttpClient httpClient = new(); static void Main(string[] args) { var timer = new Stopwatch(); timer.Start(); try { Task.WhenAll(ExecuteRequests()).Wait(); } catch (AggregateException ex) { // 捕获并打印所有任务异常 foreach (var innerEx in ex.InnerExceptions) { Console.WriteLine($"请求异常: {innerEx.Message}"); } } timer.Stop(); foreach (var item in pi.OrderBy(x => x.Key)) Console.Write(item.Value); Console.WriteLine($"\n\n耗时: {timer.ElapsedMilliseconds}ms"); Console.WriteLine($"有效条目数: {pi.Count}"); } static List<Task> ExecuteRequests() { var tasks = new List<Task>(); for (int i = 0; i < TotalRequests; i++) { tasks.Add(Task.Run(async () => { int currentRequestId; // 锁保护请求ID生成 lock (lockObj) { currentRequestId = requestsCount++; } try { var result = await httpClient.GetAsync($"https://api.pi.delivery/v1/pi?start={currentRequestId * 1000}&numberOfDigits=1000"); result.EnsureSuccessStatusCode(); // 非200状态直接抛出异常 var json = await result.Content.ReadAsStringAsync(); var content = JsonSerializer.Deserialize<JsonObject>(json)!["content"]!.ToString(); // 锁保护字典写入操作 lock (lockObj) { pi.Add(currentRequestId, content); } } catch (Exception ex) { Console.WriteLine($"请求ID {currentRequestId} 失败: {ex.Message}"); } })); } return tasks; }
关键修改说明
- 复用HttpClient:全局创建一个
HttpClient实例,避免频繁创建销毁导致的连接耗尽问题,同时提升请求效率。 - 锁保护字典操作:对
pi.Add也添加锁,确保字典的线程安全访问,避免集合内部状态异常。 - 异常处理:
- 添加
try-catch捕获单个任务的请求异常,记录失败请求的ID和原因。 - 使用
result.EnsureSuccessStatusCode()将非200状态码转为异常,统一处理失败场景。 - 外层捕获
AggregateException,打印所有任务的异常信息,便于排查问题。
- 添加
- 移除冗余线程池校验:删除手动等待线程池可用线程的逻辑,交由
Task.Run自动管理线程调度。
内容的提问来源于stack exchange,提问作者Daniel Mendes
相关产品推荐
相关产品推荐

