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

使用SemaphoreSlim运行并行任务的性能困惑与优化咨询

问题描述

我用HtmlAgilityPack和SemaphoreSlim实现异步批量发送HTTP请求并收集响应,遇到了性能瓶颈:

  • 1000个任务、SemaphoreSlim初始计数15时,总耗时约40秒;
  • 任务量翻倍到2000个时,耗时线性增长到约80秒;
  • 但同时启动两个控制台应用,各处理1000个任务(Semaphore初始计数均为15),每个应用耗时约60秒,2000个任务总耗时仅60秒。

想知道怎么在单个应用内达到这种性能,以及如何进行性能扩容。

相关代码

Main方法代码

using appParser.Services;
using HtmlAgilityPack;
using System.Diagnostics;

internal class Program
{
        static int j;
        static int k;
        static long sum=0;
        static HtmlWeb web = new HtmlWeb();

        static async Task Main(string[] args)
        {
            web.UserAgent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Mobile Safari/537.36";
            
            List<Task> tasks = new List<Task>();
            List<HtmlDocument> htmldocs = new();

            var stopwatch = new Stopwatch();

            Console.WriteLine("Put request number");
            k = Int32.Parse(Console.ReadLine());
            Console.WriteLine("Put initCount");
            j = Int32.Parse(Console.ReadLine());
            var PerformanceTest = new PerfrormanceTestSemaphore(j);
            Console.WriteLine("-------------------------------------------------------------");
            Console.WriteLine($"InitCount: {j}");
            stopwatch.Start();

            for (int i = 0; i < k; i++)
            {
                tasks.Add(PerformanceTest.LoadDocs("https://m.olx.pl/elektronika/gry-konsole/q-xbox/?search%5Border%5D=created_at:desc", web));
            }
            stopwatch.Stop();
            Console.WriteLine($"Tasks added in {stopwatch.ElapsedMilliseconds} ms");
            stopwatch.Restart();
                
            while (tasks.Count > 0)
            {
                stopwatch.Start();
                Task finished = await Task.WhenAny(tasks);
                htmldocs.Add(((Task<HtmlDocument>)finished).Result);
                tasks.Remove(finished);
                stopwatch.Stop();
                Console.WriteLine($"{tasks.Count} left, last finished in {stopwatch.ElapsedMilliseconds} ms");
                sum += stopwatch.ElapsedMilliseconds;
                stopwatch.Restart();
            }
            Console.WriteLine("-------------------------------------------------------------");
            Console.WriteLine($"Tasks finished in {sum} ms");
            Console.WriteLine("-------------------------------------------------------------");
            await File.WriteAllTextAsync("html.txt", htmldocs[k-1].Text.ToString());
            Console.ReadLine();
        }
}

PerfrormanceTestSemaphore类代码

using HtmlAgilityPack;

namespace appParser.Services
{
    internal class PerfrormanceTestSemaphore
    {
        public int SemaphoreNum { get; set; }
        private SemaphoreSlim _mutex;
        
        CancellationTokenSource cts = new();
        public PerfrormanceTestSemaphore(int semaphoreNum)
        {
            SemaphoreNum = semaphoreNum;
            _mutex = new SemaphoreSlim(semaphoreNum);
        }

        public async Task<HtmlDocument> LoadDocs(string url, HtmlWeb web)
        {
            await _mutex.WaitAsync(cts.Token);
            try
            {
                string loadUrl = url + "&view=list/full_page=True";
                return await web.LoadFromWebAsync(url);
            }
            finally
            {
                _mutex.Release();
            }
        }
    }
}
优化方案

1. 替换HtmlWeb为复用的HttpClient

HtmlWeb内部默认的HttpClient实例复用性差,每个请求可能创建新连接,导致连接池耗尽。改用单例HttpClient(或IHttpClientFactory)复用连接:

// 全局复用的HttpClient
private static readonly HttpClient _httpClient = new HttpClient(new HttpClientHandler
{
    MaxConnectionsPerServer = 30, // 匹配目标域名的最大并发连接数
    UseCookies = true,
    AllowAutoRedirect = true
})
{
    DefaultRequestHeaders =
    {
        {"User-Agent", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Mobile Safari/537.36"}
    }
};

// 替换原LoadDocs方法
public async Task<HtmlDocument> LoadDocs(string url)
{
    await _mutex.WaitAsync(cts.Token);
    try
    {
        string loadUrl = url + "&view=list/full_page=True";
        var response = await _httpClient.GetStringAsync(loadUrl);
        var doc = new HtmlDocument();
        doc.LoadHtml(response);
        return doc;
    }
    finally
    {
        _mutex.Release();
    }
}

2. 优化Task.WhenAny的低效循环

Task.WhenAny配合List<Task>.Remove是O(n)复杂度,任务越多越慢。改用Task.WhenAll批量等待所有任务完成:

// 替换原while循环
var allTasks = tasks.Cast<Task<HtmlDocument>>().ToArray();
await Task.WhenAll(allTasks);
htmldocs.AddRange(allTasks.Select(t => t.Result));

3. 匹配Semaphore并发数与连接池容量

单个应用内,Semaphore初始计数不要低于MaxConnectionsPerServer,否则会浪费连接池资源。比如将Semaphore初始计数设为30,和MaxConnectionsPerServer一致,同时可根据目标网站限流策略、本地CPU核心数调整。

4. 多分区并行处理(模拟多应用效果)

拆分任务为多个独立的并发队列,每个队列用单独的Semaphore,相当于在单个应用内模拟多个独立请求池:

var semaphore1 = new SemaphoreSlim(15);
var semaphore2 = new SemaphoreSlim(15);

var tasks = new List<Task<HtmlDocument>>();
// 第一组任务
for (int i = 0; i < 1000; i++)
{
    tasks.Add(LoadDocsWithSemaphore(url, semaphore1));
}
// 第二组任务
for (int i = 0; i < 1000; i++)
{
    tasks.Add(LoadDocsWithSemaphore(url, semaphore2));
}

await Task.WhenAll(tasks);

5. 线程安全的结果收集

原代码中List<HtmlDocument>不是线程安全的,多任务同时添加会导致异常。改用ConcurrentBag<HtmlDocument>:

var htmldocs = new ConcurrentBag<HtmlDocument>();
// 在任务完成时添加结果
htmldocs.Add(await finishedTask);

6. 系统级连接配置(仅.NET Framework)

如果使用.NET Framework,调整ServicePointManager参数提升连接复用效率:

ServicePointManager.DefaultConnectionLimit = 100;
ServicePointManager.Expect100Continue = false;

内容的提问来源于stack exchange,提问作者mrRemote

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 17:10:32