.NET Core域名检测:PLINQ性能差与信号量结果缺失问题求助
针对.NET Core域名活跃检测项目的性能优化与问题排查方案
先梳理下你遇到的三个核心问题,逐个拆解并给出接地气的解决方案:
问题1:PLINQ实现仅占用单线程,性能极差
你明明设置了WithDegreeOfParallelism(7)和ForceParallelism,却还是单线程跑,核心原因有两个:
- HttpWebRequest连接数限制:.NET默认
ServicePointManager.DefaultConnectionLimit是2,就算开了多线程,所有请求也会排队等可用连接,看起来就像单线程在干活。 - 同步阻塞拖垮线程池:
IsDomainActive里用了同步的GetResponse(),会死死阻塞线程池线程。线程池为了避免过度创建线程,会缓慢扩容,直接导致并行度上不去。
修复建议:
- 启动时先调整连接限制:
ServicePointManager.DefaultConnectionLimit = 200; // 和你计划的并发数匹配 - 把同步的
HttpWebRequest换成异步HttpClient调用(你已经在更新代码里做了,但要注意复用实例),别再阻塞线程池。 - 放弃用PLINQ处理异步任务:PLINQ对异步方法支持很差,用
Task.WhenAll实现异步并行才是正确姿势。
问题2:Semaphore版本结果数量与输入不匹配
这个问题根儿上就是你没等所有启动的Task完成就结束流程了!看你的代码,foreach里只启动了一堆Task,完全没加等待逻辑,直接进finally释放Semaphore,此时很多任务还没跑完,自然不会把结果塞进allDomains列表。
修复建议:
- 收集所有Task,在finally前等它们全部完成:
var tasks = new List<Task>(); foreach (var domain in domainList) { var task = Task.Factory.StartNew(() => { // 原有的任务逻辑... }, TaskCreationOptions.LongRunning); tasks.Add(task); } // 等所有任务跑完再继续 Task.WaitAll(tasks.ToArray()); - 把
lock(allDomains)换成线程安全集合ConcurrentBag<BsonDocument>,彻底去掉锁竞争:var allDomains = new ConcurrentBag<BsonDocument>(); // 任务里直接加,不用锁 allDomains.Add(new BsonDocument { ... }); - 检查是否有任务静默失败:用
Task.WhenAll替代WaitAll,捕获AggregateException,确保异常不会导致任务悄悄挂掉。
问题3:HttpClient+ConcurrentBag的性能问题
你的更新代码里有几个致命的性能坑:
- 每个请求新建HttpClient:频繁创建HttpClient会导致套接字耗尽(出现大量TIME_WAIT连接),这是.NET里用HttpClient的经典误区。HttpClient是线程安全的,全局复用一个实例就行,或者用
HttpClientFactory。 - 用
.Result阻塞异步方法:PLINQ里调用IsDomainActive(domain).Result会把线程池线程卡死,导致线程池饥饿,完全浪费了异步的优势。 - 超时设置错了:你加的
TimeoutHeader不是标准HTTP头,HttpClient根本不认。正确做法是用CancellationTokenSource设置请求超时。 - 每个请求新建HttpClientHandler和WebProxy:这些资源创建开销极大,必须复用。
优化后的异步并行方案示例:
// 全局复用的HttpClient实例(或者用HttpClientFactory) private static readonly HttpClient _httpClient; static YourClassName() { var netProxy = new WebProxy("<proxyserver>", port) { Credentials = new NetworkCredential("<login>", "<password>") }; var handler = new HttpClientHandler() { Proxy = netProxy, UseProxy = true, MaxConnectionsPerServer = 200 // 单服务器最大连接数,和并发数匹配 }; _httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(5) // 全局超时 }; _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.121 Safari/535.2"); } private static async Task<BsonDocument> CheckDomainAsync(string domain) { var url = $"http://{domain}"; bool isActive = false; try { using var request = new HttpRequestMessage(HttpMethod.Get, url); // 只读取响应头,节省带宽 using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); isActive = response.IsSuccessStatusCode; } catch { isActive = false; } return new BsonDocument { {"Url", domain}, {"Active", isActive}, {"CreatedOn", DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Local)}, {"UpdatedOn", DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Local)} }; } // 批量处理域名的方法 private static async Task<List<BsonDocument>> ProcessDomainsAsync(IEnumerable<string> domains, CancellationToken cancellationToken) { // 把每个域名转换成异步任务 var tasks = domains.Select(domain => CheckDomainAsync(domain)); // 等所有任务完成,拿结果 var results = await Task.WhenAll(tasks).ConfigureAwait(false); return results.ToList(); }
通用性能优化与排查技巧
- 监控资源使用:
- 用
netstat -ano看套接字情况,如果有大量TIME_WAIT,铁定是HttpClient没复用。 - 用Windows任务管理器或Linux
top看线程数,如果线程数远低于你设置的并发数,要么是线程池被阻塞,要么是连接数受限。
- 用
- 调整并发数:8核Windows机器设200-300并发,4核Ubuntu设100-150,别贪多把系统资源榨干。
- 异常日志:给每个失败的域名加日志,方便排查为啥结果缺失,避免任务静默失败。
- 减少不必要IO:别频繁调用
Console.WriteLine,这同步IO开销很大,测试时可以关掉,正式环境用异步日志库(比如Serilog)。
内容的提问来源于stack exchange,提问作者s vinayagam
相关产品推荐
相关产品推荐

