- html - 出于某种原因,IE8 对我的 Sass 文件中继承的 html5 CSS 不友好?
- JMeter 在响应断言中使用 span 标签的问题
- html - 在 :hover and :active? 上具有不同效果的 CSS 动画
- html - 相对于居中的 html 内容固定的 CSS 重复背景?
我正在运行一个具有 24 个线程 (5900X) 的 CPU,启动 20 个任务来执行一个应该完全受 CPU 限制但 CPU 负载峰值为 10% 的操作。试图看看是否有人可以阐明这是我误解了任务如何线程化,还是执行处理的库 (HtmlAgilityPack) 有问题?
这是一个有点复杂的例子:
public async static Task TestHtmlAgilityPack(bool loadHtml = true)
{
// "basePath" is a folder has approx 20 folders each containing approx 3000 files (20 tasks * 3,000 files = 60k overall)
var dirs = Directory.GetDirectories(basePath);
List<Task> tasks = new();
var strs = new ConcurrentBag<string>();
foreach (var dir in dirs)
{
tasks.Add(Task.Run(() =>
{
foreach (var file in Directory.GetFiles(dir, "*.html")) // Each of the 20 tasks processes approx 3000 files
{
var html = File.ReadAllText(file);
strs.Add(html.Substring(1, 1000));
if (loadHtml)
{
var doc = new HtmlDocument();
doc.LoadHtml(html);
}
}
}));
}
await Task.WhenAll(tasks);
Console.WriteLine(strs.Last());
}
如果我在没有 LoadHtml 的情况下运行它,它将在 15 秒内完成,因此 IO 访问时间是微不足道的。使用 LoadHtml 现在需要 20 分钟,我知道将 HTML 解析为可查询的形式需要时间,这很好/预期,但令人困惑的是它(应该?)是一个纯粹的 CPU 密集型操作,它不等待任何东西。为什么在 24 线程 CPU 上通过 CPU 密集型操作加载 20 个线程时,CPU 峰值达到 10% 而不是接近 80%?
这是否表明 LoadHtml 方法或其他方法效率低下?
最佳答案
此代码存在几个限制其可扩展性的问题。
ReadAllText
在只需要 1000 个字符时读取整个文件。字符串是不可变的,因此 html.Substring(1,1000)
生成一个新 子字符串。所有这些都会占用内存,并且必须在某个时刻进行垃圾回收。ConcurrentBag
不是像 ConcurrentQueue 或 ConcurrentDictionary 这样的通用并发集合。它使用线程本地存储来确保创建项目的线程可以比其他线程更快地检索项目。.NET 提供了几个高级类,可用于构建比加载、解析和导入文件复杂得多的解析管道。这些包括 Dataflow blocks , Channels和 Async Streams/IAsyncEnumerable .
改进问题代码的一种方法是使用 Dataflow block 来枚举根文件夹、加载文件内容并在具有不同并行度的不同 block 中解析它。
首先,可以将抓取、加载和解析代码提取到单独的方法中:
record Search(DirectoryInfo root,string pattern);
record Sample(FileInfo file,string sample);
record SampleHtml(FileInfo file,HtmlDocument html);
IEnumerable<FileInfo> Crawl(Search search)
{
var (root,pattern)=search;
var searchOptions=new EnumerationOptions {
RecurseSubdirectories=true,
IgnoreInaccessible=true
};
return root.EnumerateFiles(pattern,searchOptions);
}
async Task<Sample> ReadSample(FileInfo file,int length)
{
var buffer=new char[length+1];
using var reader=file.OpenText();
var count=await reader.ReadBlockAsync(buffer,length+1);
var html= new String(buffer,1,count-1);
return new Sample(file,html);
}
SampleHtml ParseSample(Sample sample)
{
var html=new HtmlDocument();
html.LoadHtml(sample.sample);
return new SampleHtml(sample.file,html);
}
数据流 block 可用于创建管道:
var loadOptions=new ExecutionDataflowBlockOptions{
MaxDegreeOfParallelism=2,
BoundedCapacity=1
};
var parseOptions=new ExecutionDataflowBlockOptions{
MaxDegreeOfParallelism=4,
BoundedCapacity=1
};
var crawler=new TransformManyBlock<Search,FileInfo>(search=>
Crawl(search);
var loader = new TransformBlock<FileInfo,Sample>(file=>
ReadSample(file,1000),loadOptions);
var parserBlock=new TransformBlock<Sample,SampleHtml>(sample=>
ParseHtml(sample),parseOptions);
var results=new BufferBlock<SampleHtml>();
var linkOptions=new DataflowLinkOptions {
PropagateCompletion = true
};
crawler.LinkTo(loader,linkOptions);
loader.LinkTo(parser,linkOptions);
//Don't propagate completion, we just cache results here
parser.Linkto(results);
为了使用管道,我们将搜索规范发布到头部 block crawler
并等待直到最后一个 block ,解析器,完成所有处理
var root=new DirectoryInfo(path_to_root);
var pattern="*.html";
await crawler.SendAsync(new Search(root,pattern));
crawler.Complete();
await parser.Completion;
这一点 results
包含所有结果。我们可以使用TryReceive
一个接一个地弹出项目或TryReceiveAll将所有内容读入容器:
if(results.TryReceiveAll(out var docs)
{
var last=docs[^1];
}
loader
和 parser
block 的 BoundedCapacity
为 1。这意味着它们的输入缓冲区将只接受超出正在处理的项目的单个项目.任何上游 block 都必须在发布新项目之前等待,一直到爬虫。这可以防止内存中充满处理速度不够快的对象。
重复使用缓冲区
ArrayPool类可以提供可重用的缓冲区,从而避免为每个文件创建新的 char[1001]
缓冲区。加载程序 DOP 为 4,这意味着我们只需要 4 个缓冲区而不是 3000 个缓冲区:
async Task<Sample> ReadSample(FileInfo file,int length)
{
var buffer=ArrayPool<char>.Shared.Rent(length+1);
try
{
using var reader=file.OpenText();
var count=await reader.ReadBlockAsync(buffer,0,length+1);
var html= new String(buffer,1,count-1);
return new Sample(file,html);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
}
这剩下 3000 个 1000 个字符的字符串对象。如果将加载器和解析器修改为传递 byte[]
缓冲区而不是字符串,则可以消除这些问题。 HtmlAgilityPack 的 Load
可以从任何 Stream
或 StreamReader
读取,这意味着它也可以从包装缓冲区的 MemoryStream 加载。
唯一的问题是 UTF8 对每个字符使用的字节数可变,因此无法提前猜测准确读取 1000 个字符需要多少字节。如果 HTML 文件预计包含大量非英语文本,则必须增加 ReadSample
的长度。
record Sample(FileInfo file,byte[] buffer,int count);
async Task<Sample> ReadSample(FileInfo file,int length)
{
var buffer=ArrayPool<char>.Shared.Rent(length+1);
using var reader=file.OpenText();
var count=await reader.ReadBlockAsync(buffer,0,length+1);
return new Sample(file,buffer,count);
}
SampleHtml ParseSample(Sample sample)
{
try
{
var html=new HtmlDocument();
using var ms=new MemoryStream(sample.buffer,1,sample.count);
html.Load(ms);
return new SampleHtml(sample.file,html);
}
finally
{
ArrayPool<char>.Shared.Return(sample.buffer);
}
}
关于c# - 仅使用一小部分 CPU 在多个任务中运行 CPU 密集型方法?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73182184/
我想了解 Ruby 方法 methods() 是如何工作的。 我尝试使用“ruby 方法”在 Google 上搜索,但这不是我需要的。 我也看过 ruby-doc.org,但我没有找到这种方法。
Test 方法 对指定的字符串执行一个正则表达式搜索,并返回一个 Boolean 值指示是否找到匹配的模式。 object.Test(string) 参数 object 必选项。总是一个
Replace 方法 替换在正则表达式查找中找到的文本。 object.Replace(string1, string2) 参数 object 必选项。总是一个 RegExp 对象的名称。
Raise 方法 生成运行时错误 object.Raise(number, source, description, helpfile, helpcontext) 参数 object 应为
Execute 方法 对指定的字符串执行正则表达式搜索。 object.Execute(string) 参数 object 必选项。总是一个 RegExp 对象的名称。 string
Clear 方法 清除 Err 对象的所有属性设置。 object.Clear object 应为 Err 对象的名称。 说明 在错误处理后,使用 Clear 显式地清除 Err 对象。此
CopyFile 方法 将一个或多个文件从某位置复制到另一位置。 object.CopyFile source, destination[, overwrite] 参数 object 必选
Copy 方法 将指定的文件或文件夹从某位置复制到另一位置。 object.Copy destination[, overwrite] 参数 object 必选项。应为 File 或 F
Close 方法 关闭打开的 TextStream 文件。 object.Close object 应为 TextStream 对象的名称。 说明 下面例子举例说明如何使用 Close 方
BuildPath 方法 向现有路径后添加名称。 object.BuildPath(path, name) 参数 object 必选项。应为 FileSystemObject 对象的名称
GetFolder 方法 返回与指定的路径中某文件夹相应的 Folder 对象。 object.GetFolder(folderspec) 参数 object 必选项。应为 FileSy
GetFileName 方法 返回指定路径(不是指定驱动器路径部分)的最后一个文件或文件夹。 object.GetFileName(pathspec) 参数 object 必选项。应为
GetFile 方法 返回与指定路径中某文件相应的 File 对象。 object.GetFile(filespec) 参数 object 必选项。应为 FileSystemObject
GetExtensionName 方法 返回字符串,该字符串包含路径最后一个组成部分的扩展名。 object.GetExtensionName(path) 参数 object 必选项。应
GetDriveName 方法 返回包含指定路径中驱动器名的字符串。 object.GetDriveName(path) 参数 object 必选项。应为 FileSystemObjec
GetDrive 方法 返回与指定的路径中驱动器相对应的 Drive 对象。 object.GetDrive drivespec 参数 object 必选项。应为 FileSystemO
GetBaseName 方法 返回字符串,其中包含文件的基本名 (不带扩展名), 或者提供的路径说明中的文件夹。 object.GetBaseName(path) 参数 object 必
GetAbsolutePathName 方法 从提供的指定路径中返回完整且含义明确的路径。 object.GetAbsolutePathName(pathspec) 参数 object
FolderExists 方法 如果指定的文件夹存在,则返回 True;否则返回 False。 object.FolderExists(folderspec) 参数 object 必选项
FileExists 方法 如果指定的文件存在返回 True;否则返回 False。 object.FileExists(filespec) 参数 object 必选项。应为 FileS
我是一名优秀的程序员,十分优秀!