C#并发任务优化咨询:IIS中REST API批量处理100请求性能提升
首先,你的核心问题在于用同步I/O+Task.Run的方式浪费了线程池资源——Task.Run会把同步的I/O操作放到线程池线程上执行,但线程在等待第三方服务响应时是被阻塞的,无法处理其他请求,这就是为什么你观察到只有7-8个线程在工作(线程池默认最小线程数有限,而且阻塞会导致线程池扩容很慢)。要达到2秒内处理100个请求的目标,必须转向异步I/O优先的方案,让线程在I/O等待时被释放,从而支持更高的并发量。
第一步:把PerformLogic改成异步方法
因为你的PerformLogic里主要是依赖式的I/O操作(先调用服务1,再调用服务2),完全可以用async/await重构为异步方法,这样线程不会在等待I/O时被占用:
// 重构为异步方法,返回Task<FlightInformation> public async Task<FlightInformation> PerformLogicAsync(Request request, ...) { // 1. 异步调用第一个第三方Web服务(替换你的同步调用为异步版本) var detail = await CallThirdPartyService1Async(request); // 2. 基于上一步结果异步调用第二个第三方Web服务 var anotherDetail = await CallThirdPartyService2Async(detail); // 3. CPU绑定的转换计算(这里如果计算量不大,直接同步执行即可;如果计算很重,可以考虑用Task.Run,但优先保持在当前上下文) return TransformResult(detail, anotherDetail); }
注意:确保调用第三方服务的方法本身是异步的(比如用HttpClient.GetAsync而不是HttpClient.Get),如果第三方服务的SDK只有同步方法,那可以用await Task.Run(() => SyncThirdPartyCall())包裹,但这是退而求其次的方案,优先用原生异步API。
第二步:重构批量处理代码,使用Task.WhenAll而非Task.WaitAll
原来的Task.WaitAll会阻塞当前线程,而await Task.WhenAll是异步等待,不会占用主线程,同时配合异步的PerformLogicAsync,能让线程池高效利用资源:
// 用LINQ生成异步任务列表 var tasks = multiFlightStatusRequest.FlightRequests .Select(req => PerformLogicAsync(req, lite, fetchRouteByAnyLeg)) .ToList(); // 异步等待所有任务完成,直接得到结果数组 var flightInformations = await Task.WhenAll(tasks);
第三步:调整线程池最小线程数(辅助优化)
线程池默认的最小工作线程数可能比较低(比如和CPU核心数一致),而异步I/O场景下,我们需要更多的线程来处理并发的异步回调。可以在应用启动时(比如Global.asax的Application_Start或者ASP.NET Core的Program.cs)设置:
// 根据你的服务器配置和第三方服务的并发限制调整,比如设置为32/32 ThreadPool.SetMinThreads(32, 32);
这个设置让线程池一开始就有足够的线程处理并发请求,避免线程池扩容的延迟(线程池默认会每隔500ms才新增一个线程)。
第四步:用SemaphoreSlim控制并发度(关键)
虽然异步I/O能支持很高的并发,但第三方服务可能有QPS或并发连接的限制,直接发100个请求可能会被限流或者导致服务端压力过大。用SemaphoreSlim可以精准控制并发请求数:
// 设置允许的最大并发数,比如20(根据第三方服务的承受能力调整) var semaphore = new SemaphoreSlim(20); var tasks = multiFlightStatusRequest.FlightRequests .Select(async req => { await semaphore.WaitAsync(); // 等待获取信号量 try { return await PerformLogicAsync(req, lite, fetchRouteByAnyLeg); } finally { semaphore.Release(); // 释放信号量,让其他任务可以执行 } }) .ToList(); var flightInformations = await Task.WhenAll(tasks);
通过调整SemaphoreSlim的初始计数,你可以找到一个平衡点:既足够高以达到2秒的目标(比如20个并发的话,100个请求大概需要5轮,每轮300ms,总时间约1.5秒,加上计算时间应该能控制在2秒内),又不会超过第三方服务的限制。
为什么这样能解决问题?
- 异步I/O让线程在等待第三方服务响应时被释放,线程池可以用同一个线程处理多个异步请求的回调,从而支持远高于线程数的并发量。
- 避免了
Task.Run带来的线程阻塞,原来的方式每个请求占用一个线程直到完成,而异步方式下一个线程可以处理多个请求的非阻塞阶段。 - 精准控制并发度既保证了性能,又避免了对第三方服务的冲击。
内容的提问来源于stack exchange,提问作者Developer

