HtmlAgilityPack 是一个 HTML 解析库,用于 .NET 平台。它允许开发者以类似于解析 XML 的方式,轻松地解析和操作 HTML 文档。这个库特别适合处理非标准的 HTML,例如那些格式不正确或包含错误的 HTML 文档。
从原理上说,解析是一个 CPU 密集型操作。在计算资源充裕的情况下,使用多线程并行可以加快处理速度。
以下代码展示了两个场景:
- 使用一个线程解析 1000 个页面
- 使用 8 个线程解析 1000 个页面(总量 1000 个,测试机器上的 CPU 有 8 个内核)。
string html = File.ReadAllText("PATH"); //One thread for (int i = 0; i < 1000; i++) new HtmlDocument().LoadHtml(html); //Several threads Parallel.For(0, 1000, (int i) => new HtmlDocument().LoadHtml(html));
然而实际的情况是:尽管多线程版本消耗了 2 ~ 3 倍的 CPU,但所花费的时间大致相同。而且 CPU 占用率一直维持在 30% 以下。即便更换了要处理的页面,或者内核数量更多的电脑,情况都差不多。
如何解决
开启更多的线程并不会提升处理的速度,这让我开始怀疑是不是存在锁的问题。遗憾的是没有在源代码中找到 lock ,但是发现了一个 Issues:
https://github.com/zzzprojects/html-agility-pack/issues/191
用户 BenoitSa 提到:
在使用 Profiler 工具对多线程程序进行分析之后,发现程序可能存在内存瓶颈。根据他的观察,有大约 50% 的 CPU 时间耗费在了内存分配上。
这和使用的 GC 类型有关,向 App.config 增加以下代码可以解决该问题:
<runtime> <gcServer enabled="true"/> <gcConcurrent enabled="false" /> </runtime>
适配 .NET 8.0
我的程序是一个使用 .NET 8.0 框架的控制台,增加 App.config 文件之后并没有效果。于是,我找到了微软的官方文档:
https://learn.microsoft.com/zh-cn/dotnet/core/runtime-config/garbage-collector
根据文档所述,可以通过环境变量、runtimeconfig.json 文件或项目文件来指定程序使用 Server 版本。我选择修改项目文件:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <ServerGarbageCollection>true</ServerGarbageCollection> </PropertyGroup> </Project>
问题得以解决:处理速度快了不少,CPU 占用维持在了 70% 左右。