如何使用C#快速批量发送高并发WebService请求
原有代码的核心问题
- 并发上不去的根因不是ThreadPool默认线程数少:.NET线程池的工作线程注入是缓慢爬坡的,且你当前用同步方式跑网络IO请求,每个等待响应的过程都会独占一个工作线程,属于IO密集型场景下的典型误用,线程资源利用率极低。
- 存在严重线程安全bug:
returnQuery是普通字符串类型,多线程下无锁执行+=拼接会出现内容丢失、数据错乱,极端情况会触发运行时异常。 - 等待逻辑低效且脆弱:每秒轮询任务状态会引入最多1秒的无效等待,且如果某个请求抛出异常、
StatusCode永远不会变为Created,整个循环会直接死锁。 - 无限制投递任务会引发反向问题:如果单次查询返回成百上千条待处理数据,瞬间发起的海量请求要么触发目标WebService的限流策略,要么耗尽本机TCP端口资源,反而会大幅拉低整体处理速度。
优化方案
1. 用异步IO替代同步线程池请求
网络请求属于IO等待型操作,全程不需要占用工作线程等待响应,用async/await实现异步HTTP调用,仅需极少的线程即可支撑上百个并发请求,效率远高于同步阻塞线程池的方案。注意要全局复用HttpClient实例,不要每次请求新建对象,避免TCP端口耗尽。
2. 加入并发数控制
用SemaphoreSlim实现并发闸门,根据目标WebService的承载能力设置最大并发值,建议初始值设为10-20,压测后逐步调整到最优值,避免触发对方限流。
3. 用原生任务等待机制替代轮询
所有请求封装为异步任务后,用Task.WhenAll等待全部任务执行完成,任务结束后立刻进入下一步流程,没有轮询带来的额外等待开销。
4. 修复线程安全问题
多线程下收集结果时,用加锁的StringBuilder或者线程安全集合存储每个请求生成的SQL语句,避免数据错乱丢失。
优化后参考代码
// 全局复用HttpClient实例,禁止在方法内每次新建 private static readonly HttpClient _httpClient = new HttpClient(); // 根据WebService承载能力调整最大并发数,建议从10开始压测调整 private const int MaxConcurrentRequests = 20; private readonly SemaphoreSlim _concurrencySemaphore = new SemaphoreSlim(MaxConcurrentRequests); public async Task DoTransitTimeLoopAsync(string botName) { while (keepProcessingTransitTime) { // 查询待处理数据 DataTable dt = sendSqlQuery($"execute fex.transitStart @Bot='{botName}', @TransitCount={fexTransitInstaces}"); List<TransitTimePackage> packages = new List<TransitTimePackage>(); foreach (DataRow row in dt.Rows) { packages.Add(TransitTimePackage.createFromDataRow(this, row)); } StringBuilder returnQueryBuilder = new StringBuilder(); List<Task> processTasks = new List<Task>(); foreach (var package in packages) { processTasks.Add(ProcessSinglePackageAsync(package, returnQueryBuilder)); } // 等待所有请求处理完成,无需轮询 await Task.WhenAll(processTasks); string finalReturnQuery = returnQueryBuilder.ToString(); // 结果写回数据库 if (!string.IsNullOrWhiteSpace(finalReturnQuery)) { sendSqlQuery(finalReturnQuery); } } } private async Task ProcessSinglePackageAsync(TransitTimePackage package, StringBuilder queryBuilder) { await _concurrencySemaphore.WaitAsync(); try { // 将原同步请求方法改为异步实现,内部使用HttpClient.SendAsync发送请求 RestResponse res = await package.SendWebserviceRequestAsync(); package.ParseResponse(res); // 加锁保证字符串拼接线程安全 lock (queryBuilder) { queryBuilder.AppendLine(package.sqlQuery); } } catch (Exception ex) { // 单个请求失败不影响整批处理,标记状态+记录日志即可 package.StatusCode = HttpStatusCode.InternalServerError; // 此处可添加失败日志记录逻辑 } finally { _concurrencySemaphore.Release(); } }
额外优化建议
- 不要随意修改ThreadPool线程数配置:很多人遇到并发问题第一反应是调大线程池最大/最小线程数,对于IO密集型场景这是典型的治标不治本,过多的工作线程会带来大量上下文切换开销,反而会降低整体性能,异步IO才是正确解法。
- 控制单批次处理量:如果单次查询返回的待处理数据超过100条,建议分批次处理,比如每批处理50条,避免单批次等待时间过长引发数据库连接超时、WebService请求超时等问题。
- 提前确认WebService限流规则:大部分对外接口都会配置单IP的QPS限制,超过阈值会直接返回429错误,将并发数调整到对方允许的上限即可,不要盲目拉高并发导致大量请求失败。
- 优化数据库写回逻辑:如果单次要写回的记录量很大,不要拼超长的多段SQL执行,可以用表值参数、SqlBulkCopy等方式做批量写入,执行效率比拼接SQL高一个量级。
内容的提问来源于stack exchange,提问作者ebeigh
相关产品推荐
相关产品推荐

