【问题标题】:C# Parallel.ForEach() memory usage keeps growingC# Parallel.ForEach() 内存使用量不断增长
【发布时间】:2020-03-30 10:57:10
【问题描述】:
public string SavePath { get; set; } = @"I:\files\";

public void DownloadList(List<string> list)
{
    var rest = ExcludeDownloaded(list);
    var result = Parallel.ForEach(rest, link=>
    {
        Download(link);
    });
}

private void Download(string link)
{
    using(var net = new System.Net.WebClient())
    {
        var data = net.DownloadData(link);

        var fileName = code to generate unique fileName;
        if (File.Exists(fileName))
            return;

        File.WriteAllBytes(fileName, data);
    }
}

var downloader = new DownloaderService();
var links = downloader.GetLinks();
downloader.DownloadList(links);

我观察到该项目的 RAM 使用量不断增长

我猜 Parallel.ForEach() 有问题,但我无法弄清楚。

是否存在内存泄漏,或者发生了什么?


更新 1

改成新代码后

private void Download(string link)
{
    using(var net = new System.Net.WebClient())
    {
        var fileName = code to generate unique fileName;
        if (File.Exists(fileName))
            return;
        var data = net.DownloadFile(link, fileName);
        Track theTrack = new Track(fileName);
        theTrack.Title = GetCDName();
        theTrack.Save();
    }
}

在运行 9 小时后,我仍然观察到内存使用量增加,但使用量增长缓慢。

只是想知道,是不是因为我没有释放 theTrack 文件的内存使用?

顺便说一句,我使用ALT package 更新文件元数据,不幸的是,它没有实现 IDisposable 接口。

【问题讨论】:

  • 不一定是内存泄漏,可能是垃圾回收器没有运行。
  • File.WriteAllBytes(fileName, data); data 你有哪个尺寸的?你试过分块写吗?
  • 由于您正在并行处理项目,因此您将每个下载文件的内容同时保存在内存中 - 多个文件。这就是它增加的原因,特别是如果它们是大文件。正如@Sean 提到的,GC 很可能还没有清理干净。
  • 嗨@PavelAnikhouski 大多数时候,文件将小于1MB,但也可能超过100MB,目前最大的是155MB。怎么分块写?
  • @riQQ DownloadFile() 解决了这个问题。谢谢!

标签: c# multithreading .net-core


【解决方案1】:

使用WebClient.DownloadFile() 直接下载到一个文件,这样您就不会在内存中保存整个文件。

【讨论】:

  • 只是为了好奇,如果我使用 File.WriteAllBytes(),如何在每次使用后释放它的内存?
  • @Franva 基本上,您分配的是大字节数组。它们由 .NET 专门处理。你可以在这里阅读更多:docs.microsoft.com/en-us/dotnet/standard/garbage-collection/…
  • GC.Collect() 释放内存,如果您不再持有对对象/字节数组的任何引用。
【解决方案2】:

Parallel.ForEach 方法旨在并行处理受 CPU 限制的工作负载。下载文件是 I/O 绑定的工作负载,因此 Parallel.ForEach 不适合这种情况,因为它不必要地阻塞了 ThreadPool 线程。正确的方法是异步的,使用 async/await。发出异步 Web 请求的推荐类是 HttpClient,而对于控制并发级别,一个极好的选择是 TPL Dataflow 库。对于这种情况,使用该库中最简单的组件 ActionBlock 类就足够了:

async Task DownloadListAsync(List<string> list)
{
    using (var httpClient = new HttpClient())
    {
        var rest = ExcludeDownloaded(list);
        var block = new ActionBlock<string>(async link =>
        {
            await DownloadFileAsync(httpClient, link);
        }, new ExecutionDataflowBlockOptions()
        {
            MaxDegreeOfParallelism = 10
        });
        foreach (var link in rest)
        {
            await block.SendAsync(link);
        }
        block.Complete();
        await block.Completion;
    }
}

async Task DownloadFileAsync(HttpClient httpClient, string link)
{
    var fileName = Guid.NewGuid().ToString(); // code to generate unique fileName;
    var filePath = Path.Combine(SavePath, fileName);
    if (File.Exists(filePath)) return;
    var response = await httpClient.GetAsync(link);
    response.EnsureSuccessStatusCode();
    using (var contentStream = await response.Content.ReadAsStreamAsync())
    using (var fileStream = new FileStream(filePath, FileMode.Create,
        FileAccess.Write, FileShare.None, 32768, FileOptions.Asynchronous))
    {
        await contentStream.CopyToAsync(fileStream);
    }
}

使用HttpClient下载文件的代码并不像WebClient.DownloadFile()那么简单,但为了保持整个过程异步(从Web读取和写入磁盘),您必须这样做.


警告:异步文件系统操作目前在 .NET 中为 not implemented efficiently。为了获得最大效率,最好避免在FileStream 构造函数中使用FileOptions.Asynchronous 选项。

【讨论】:

  • @riQQ 确实如此。但是看看remarks:重要 - 我们不建议您使用 WebClient 类进行新的开发。而是使用 System.Net.Http.HttpClient 类。
  • @riQQ HttpClient 应该提供更好的性能,因为它重用了连接。这是因为您不必为每个请求都创建一个新实例,就像您对 WebClient 所做的那样。
  • 漂亮的解决方案,有丰富的解释,在答案和cmets中~!很好的例子~!谢谢!我会在午休时间尝试一下(我已经开始工作了>_
  • @Franva 如果你想进行公平的比较,你应该在Parallel.ForEach 方法之前添加这一行:ThreadPool.SetMinThreads(50, 10);,这样ThreadPool 就有 50 个线程可用于执行 50 个并发 Web 请求.如果没有这条线,您将引入由饥饿的ThreadPool 引起的隐式限制。您的测试结果表明,对于远程服务器、Web 连接或存储硬件而言,50 个并发操作太多了。我建议您尝试使用较小的并发级别。 50 似乎远非最佳。
  • 谢谢@TheodorZoulias 我会尝试并更新你。感谢您的帮助和解释。我学到了多线程编程的洞察力。
猜你喜欢
  • 2017-11-09
  • 2013-10-08
  • 2014-02-25
  • 1970-01-01
  • 2013-08-27
  • 2018-05-01
  • 1970-01-01
  • 2017-09-17
  • 2019-09-08
相关资源
最近更新 更多