You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C# Parallel.ForEach循环调用API时员工详情对象偶尔为空问题

解决Parallel.ForEach调用API时员工对象属性为空/偶尔出现null的问题

问题根源分析

  1. GetEmployeeDetails返回非null的空对象:该方法无论API调用或解析是否失败,都会返回一个新创建的ImportedEmployee实例,只是失败时属性未赋值(全为null)。你判断importedEmployee != null永远为true,导致这些空属性的对象被加入列表,看起来像是“员工对象为null”。
  2. Parallel.ForEach与Task.Run的冗余嵌套:Parallel.ForEach本身已使用线程池并行执行,内部再调用Task.Run会额外创建任务,导致线程池资源过度消耗,增加异常概率。
  3. 同步阻塞调用引发的潜在问题: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 11:03:10