使用C#并行调用外部API时性能异常缓慢的原因排查
1. HttpClient重复创建导致套接字耗尽
每次调用GetAPIData都新建HttpClient实例,会快速耗尽系统套接字资源,引发请求排队、延迟飙升。.NET中HttpClient设计为可复用的单例/静态实例,频繁创建销毁会破坏HTTP连接池的复用机制,这是性能瓶颈的核心原因之一。
修复方案:
将HttpClient改为静态实例复用,避免重复创建:
// 静态初始化一次,全局复用 private static readonly HttpClient _httpClient = new HttpClient(); static async Task GetAPIData(string url, ConcurrentBag<Stats> bag) { // 移除原有的var client = new HttpClient(); _httpClient.Timeout = TimeSpan.FromMinutes(5); // 避免设置无限超时,合理配置超时时间 var bearerToken = "xxxxxxxxxxxxxx"; // 仅在首次调用时添加Authorization头,避免重复添加报错 if (!_httpClient.DefaultRequestHeaders.Contains("Authorization")) { _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {bearerToken}"); } // 后续业务逻辑保持不变,将client替换为_httpClient }
2. ConcurrentBag高并发锁竞争
三个任务同时对ConcurrentBag执行Add操作,虽然它是线程安全的,但每次Add都涉及内部锁竞争。当数据量达到1万+时,频繁的锁竞争会大幅拖慢执行速度。
修复方案:
每个任务先将数据收集到本地普通列表(无锁操作),完成后一次性批量添加到ConcurrentBag,减少锁竞争次数:
static async Task GetAPIData(string url, ConcurrentBag<Stats> bag) { var localList = new List<Stats>(); // 本地临时列表,避免跨线程锁竞争 using (var stream = await _httpClient.GetStreamAsync(url).ConfigureAwait(false)) using (var sr = new StreamReader(stream)) using (JsonTextReader reader = new JsonTextReader(sr)) { reader.SupportMultipleContent = true; var serializer = new JsonSerializer(); while (reader.Read()) { if (reader.TokenType == JsonToken.StartObject) { var stats = serializer.Deserialize<Stats?>(reader); if (stats != null) { localList.Add(stats); // 先写入本地列表 } } } } // 批量写入ConcurrentBag,仅触发少量锁操作 foreach (var item in localList) { bag.Add(item); } }
3. 多余的Task.Run包裹异步方法
GetAPIData本身是异步方法,返回Task,使用Task.Run(() => GetAPIData(...))会额外占用线程池线程——异步IO操作本身不会阻塞线程,完全没必要多此一举,反而会增加线程调度开销。
修复方案:
直接调用异步方法,无需Task.Run包裹:
var url = "https://extrenalsite/api/stream"; var bag = ConcurrentBag<Stats>(); var task1 = GetAPIData(url, bag); var task2 = GetAPIData(url, bag); var task3 = GetAPIData(url, bag); await Task.WhenAll(task1, task2, task3);
4. 潜在的API服务端限流
Postman单次请求耗时30秒,但并行3次调用时,API服务端可能对同一IP的并发请求做了限流(比如限制并发数、降低响应优先级),导致每个请求的响应时间大幅增加。
验证与处理:
先单独运行一个任务,查看耗时是否和Postman接近。如果单个任务耗时正常,多任务变慢,基本可以确定是服务端限流。此时需要减少并行数,或联系API提供者调整限流策略。
5. Json反序列化效率优化
使用JsonTextReader逐个反序列化对象的方式,在数据量较大时效率偏低。如果API返回的是标准JSON数组,可直接反序列化为列表,避免逐行读取的开销:
using (var response = await _httpClient.GetAsync(url).ConfigureAwait(false)) { response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var statsList = JsonConvert.DeserializeObject<List<Stats>>(json); // 使用Newtonsoft.Json if (statsList != null) { foreach (var item in statsList) { bag.Add(item); } } }
内容的提问来源于stack exchange,提问作者hillcountry99

