目录
介绍
例子
Directory.EnumerateFiles来救援
评估
关于批处理的几句话
关于返回类型的几句话
结论
介绍如果想要从目录中检索文件,对于大多数情况来说,Directory.GetFiles是一个简单的答案。但是,当您处理大量数据时,您可能需要更高级的技术。
例子假设您有一个大数据解决方案,您需要处理一个包含200000个文件的目录。对于每个文件,您提取一些基本信息。
public record FileProcessingDto
{
public string FullPath { get; set; }
public long Size { get; set; }
public string FileNameWithoutExtension { get; set; }
public string Hash { get; internal set; }
}
请注意我们如何方便地在此处为DTO使用新的C# 9记录类型。
之后,我们发送提取的信息以供进一步处理。让我们用下面的代码片段来模拟它。
public class FileProcessingService
{
public Task Process(IReadOnlyCollection files,
CancellationToken cancellationToken = default)
{
files.Select(p =>
{
Console.WriteLine($"Processing {p.FileNameWithoutExtension}
located at {p.FullPath} of size {p.Size} bytes");
return p;
});
return Task.Delay(TimeSpan.FromMilliseconds(20), cancellationToken);
}
}
现在最后一部分是提取信息并调用服务。
public class Worker
{
public const string Path = @"path to 200k files";
private readonly FileProcessingService _processingService;
public Worker()
{
_processingService = new FileProcessingService();
}
private string CalculateHash(string file)
{
using (var md5Instance = MD5.Create())
{
using (var stream = File.OpenRead(file))
{
var hashResult = md5Instance.ComputeHash(stream);
return BitConverter.ToString(hashResult)
.Replace("-", "", StringComparison.OrdinalIgnoreCase)
.ToLowerInvariant();
}
}
}
private FileProcessingDto MapToDto(string file)
{
var fileInfo = new FileInfo(file);
return new FileProcessingDto()
{
FullPath = file,
Size = fileInfo.Length,
FileNameWithoutExtension = fileInfo.Name,
Hash = CalculateHash(file)
};
}
public Task DoWork()
{
var files = Directory.GetFiles(Path)
.Select(p => MapToDto(p))
.ToList();
return _processingService.Process(files);
}
}
请注意,在这里,我们以一种天真的方式行事,并通过Directory.GetFiles(Path)一次提取提取所有文件。
但是,一旦您通过以下方式运行此代码:
await new Worker().DoWork()
您会注意到结果远不能令人满意,并且应用程序正在大量消耗内存。
Directory.EnumerateFiles来救援Directory.EnumerateFiles的作用是它返回IEnumerable,从而允许我们一个一个地获取集合项。这反过来又可以防止我们在一次加载大量数据时过度使用内存。
尽管如此,正如您可能已经注意到的那样, FileProcessingService.Process包含延迟编码(我们使用简单延迟模拟的某种I/O操作)。在实际场景中,这可能是对外部 HTTP端点的调用或使用存储。这使我们得出结论,调用FileProcessingService.Process 200 000次可能效率低下。这就是为什么我们要一次性将合理批次的数据加载到内存中。
重新编写的代码如下所示:
public class WorkerImproved
{
//omitted for brevity
public async Task DoWork()
{
const int batchSize = 10000;
var files = Directory.EnumerateFiles(Path);
var count = 0;
var filesToProcess = new List(batchSize);
foreach (var file in files)
{
count++;
filesToProcess.Add(MapToDto(file));
if (count == batchSize)
{
await _processingService.Process(filesToProcess);
count = 0;
filesToProcess.Clear();
}
}
if (filesToProcess.Any())
{
await _processingService.Process(filesToProcess);
}
}
}
在这里,我们使用foreach枚举集合,一旦达到批处理的大小,我们就会处理它并刷新集合。这里唯一有趣的时刻是在我们退出循环后最后一次调用service以刷新剩余的项目。
评估Benchmark.NET产生的结果非常有说服力:
在本文中,我们浏览了软件工程中的常见模式。合理数量的批处理有助于我们克服以逐项方式工作的I/O损失和一次性将所有项目加载到内存中的过多内存消耗。
通常,在对多个项目进行I/O操作时,您应该努力使用批处理API。一旦项目数量变多,您应该考虑将这些项目分成批次。
关于返回类型的几句话在处理代码库时,我经常看到类似于以下内容的代码:
public IEnumerable Numbers => new List { 1, 2, 3 };
我认为这段代码违反了Postel的原则,随之而来的事情是,作为一个属性的消费者,我无法弄清楚我是否可以一个一个地枚举项目,或者它们是否只是一次加载到内存中。
这是我建议对返回类型更具体的原因,即:
public IList Numbers => new List { 1, 2, 3 };
批处理是一种很好的技术,可以让您优雅地处理大量数据。Directory.EnumerateFiles是允许您为包含大量文件的目录组织批处理的API。
https://www.codeproject.com/Tips/5298439/Batch-Processing-with-Directory-EnumerateFiles