C# Parallel.ForEach循环调用API时员工详情对象偶尔为空问题
解决Parallel.ForEach调用API时员工对象属性为空/偶尔出现null的问题
问题根源分析
- GetEmployeeDetails返回非null的空对象:该方法无论API调用或解析是否失败,都会返回一个新创建的
ImportedEmployee实例,只是失败时属性未赋值(全为null)。你判断importedEmployee != null永远为true,导致这些空属性的对象被加入列表,看起来像是“员工对象为null”。 - Parallel.ForEach与Task.Run的冗余嵌套:Parallel.ForEach本身已使用线程池并行执行,内部再调用
Task.Run会额外创建任务,导致线程池资源过度消耗,增加异常概率。 - 同步阻塞调用引发的潜在问题:
ServiceRequest中使用.Result阻塞异步操作,在存在同步上下文的环境中容易引发死锁,也可能导致API请求超时或失败。
解决方案
1. 修复GetEmployeeDetails,失败时返回null
修改该方法,仅当API调用和解析完全成功时才返回员工对象,失败则返回null:
public ImportedEmployee GetEmployeeDetails(string empNum) { try { var responseString = ServiceRequest($"Employees/{empNum}", null, HttpVerbs.Get); if (string.IsNullOrEmpty(responseString)) return null; JObject result = JObject.Parse(responseString); return new ImportedEmployee { FirstName = result["FirstName"]?.ToString(), LastName = result["LastName"]?.ToString(), Username = result["LoginId"]?.ToString(), EmployeeNumber = empNum // 补充员工编号,避免丢失 }; } catch (Exception ex) { Log.Error($"Error retrieving employee details for {empNum}.", ex); return null; } }
2. 改用异步并行任务替代Parallel.ForEach+Task.Run
直接使用Task.WhenAll并行执行所有API请求,避免线程池资源浪费,同时简化代码:
public async Task<List<ImportedEmployee>> GetEmployeesAsync(List<string> empNumbers) { var employeeList = new List<ImportedEmployee>(); try { // 生成所有异步任务 var tasks = empNumbers.Select(empNum => Task.Run(() => GetEmployeeDetails(empNum))); // 等待所有任务完成 var results = await Task.WhenAll(tasks); // 过滤掉null结果,添加到列表 employeeList.AddRange(results.Where(e => e != null)); } catch (Exception ex) { Log.Error("Error retrieving employees.", ex); } return employeeList; }
3. 修复ServiceRequest的异步阻塞问题
将ServiceRequest改为异步方法,使用await替代.Result,避免死锁和阻塞:
public async Task<string> ServiceRequestAsync(string URI, string PostJSON, HttpVerbs Method) { try { using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(Configuration.ClientTimeout) }) { HttpRequestMessage request = new HttpRequestMessage(); switch (Method) { case HttpVerbs.Get: request = new HttpRequestMessage(HttpMethod.Get, BaseURL + URI); break; default: request = new HttpRequestMessage(HttpMethod.Post, BaseURL + URI); request.Content = new StringContent(PostJSON, null, "application/json"); break; } request.Headers.Add("Authorization", $"Bearer {AuthToken.Token}"); var resp = await client.SendAsync(request); resp.EnsureSuccessStatusCode(); // 确保HTTP请求成功,否则抛出异常 return await resp.Content.ReadAsStringAsync(); } } catch (Exception ex) { Log.Error($"Error in service request for URI: {URI}", ex); return null; } }
同时更新GetEmployeeDetails调用异步的ServiceRequestAsync:
public async Task<ImportedEmployee> GetEmployeeDetailsAsync(string empNum) { try { var responseString = await ServiceRequestAsync($"Employees/{empNum}", null, HttpVerbs.Get); if (string.IsNullOrEmpty(responseString)) return null; JObject result = JObject.Parse(responseString); return new ImportedEmployee { FirstName = result["FirstName"]?.ToString(), LastName = result["LastName"]?.ToString(), Username = result["LoginId"]?.ToString(), EmployeeNumber = empNum }; } catch (Exception ex) { Log.Error($"Error retrieving employee details for {empNum}.", ex); return null; } }
最终的GetEmployeesAsync也相应调整为全异步:
public async Task<List<ImportedEmployee>> GetEmployeesAsync(List<string> empNumbers) { var employeeList = new List<ImportedEmployee>(); try { var tasks = empNumbers.Select(GetEmployeeDetailsAsync); var results = await Task.WhenAll(tasks); employeeList.AddRange(results.Where(e => e != null)); } catch (Exception ex) { Log.Error("Error retrieving employees.", ex); } return employeeList; }
额外优化点
- HttpClient复用:当前
ServiceRequest每次创建新的HttpClient,建议复用单例HttpClient以避免套接字耗尽问题。 - 添加员工编号:在返回的
ImportedEmployee中设置EmployeeNumber,方便后续排查哪个员工数据获取失败。 - HTTP状态码检查:在
ServiceRequestAsync中添加resp.EnsureSuccessStatusCode(),确保捕获HTTP错误(如404、500等)。
内容的提问来源于stack exchange,提问作者M.G
相关产品推荐
相关产品推荐

