多线程的巨大内存和CPU使用量

本文关键字:CPU 使用量 内存 巨大 多线程 | 更新日期: 2023-09-27 18:27:00

我正在尝试创建一个具有播放列表选项的媒体播放器。当加载10-20首歌曲时没有问题。所以我尝试了一些要求更高的方法:我尝试加载2048首歌曲(我拍了几首歌,并复制了很多次)。试图将它们加载到我的媒体播放器中,我的CPU和Ram内存增长了95%以上(只加载了前250首歌曲),有一次我的电脑甚至重新启动了。因此,我试图通过使用一些不让应用程序接管计算机的东西来减缓操作:如果CPU负载超过85%,内存负载超过90%,我就会停止加载新歌(如果重要的话,我会使用Windows 8的64位操作系统)。不知何故,它在一开始就起了作用,允许我加载近600首歌曲,然后:

A first chance exception of type 'System.InvalidOperationException' occurred in mscorlib.dll
A first chance exception of type 'System.InvalidOperationException' occurred in mscorlib.dll
A first chance exception of type 'System.InvalidOperationException' occurred in mscorlib.dll
A first chance exception of type 'System.InvalidOperationException' occurred in mscorlib.dll
A first chance exception of type 'System.InvalidOperationException' occurred in mscorlib.dll
The thread 'vshost.NotifyLoad' (0x1d0c) has exited with code 0 (0x0).
The thread 'vshost.LoadReference' (0x1e48) has exited with code 0 (0x0).
A first chance exception of type 'System.OutOfMemoryException' occurred in mscorlib.dll
A first chance exception of type 'Microsoft.VisualStudio.Debugger.Runtime.CrossThreadMessagingException' occurred in Microsoft.VisualStudio.Debugger.Runtime.dll

最后,应用程序在"mscorlib.dll中发生类型为System.OutOfMemoryException的未处理异常"处停止。

现在来解释一下"加载歌曲"在我的应用程序中意味着什么:

  1. 一个线程,它遍历从OpenFileDialog加载的每首歌曲,并检查文件的扩展名是否已知,如果已知(在本例中为:mp3),它会在队列的末尾合并文件的路径
  2. 另一个线程验证队列中是否有任何元素
  3. 如果有,它会提取第一个元素,如果CpuLoad和MemoryLoad(由另一个线程计算)不太高,它会启动一个新线程进行一些操作(如4所示)
  4. 执行操作的线程将歌曲加载到System.Windows.Media.MediaPlayer类中,并验证以下内容:文件的TimeSpan、文件是否有音频以及文件是否有视频,并在List中记住这3个变量以及文件的路径
  5. 还有另一个线程验证是否有线程已完成其作业并已将媒体文件添加到列表中,如果有,则它将删除对它们的引用,以便垃圾收集器处理它们

下一行显示"mscorlib.dll中发生类型为System.OutOfMemoryException的未处理异常":

MediaCreator[idx].CreatorThread.Start();

这将是开始处理歌曲的线程的行。所以我做了下一件事:在上面发布的行之前,我添加了Thread.Sleep(100);。它成功了(这样做实际上加载了所有2048个文件),除了(根据我添加的Stopwatch)加载所有歌曲需要3分28秒。此外,我知道Thread.Sleep通常不是推荐的方法,我知道同样的人甚至认为这是编程技能薄弱的证明(我不知何故同意他们的观点)。我也不想使用这种方法,因为它显然需要很长时间,而且在每台计算机/cpu/hdd/ram上工作是不可信的。为了证明这一点的不可信,我测试了Sleep(10),它很快就失败了,测试了Sleen(20),它在再次失败之前加载了近1000首歌曲。我还尝试将CPU负载降低到15%,将内存负载降低到80%,但加载的歌曲不超过1300首(这也被证明是无效的,因为CPU负载有60%的短峰值)。

我还想提到的是,Winamp在30秒内加载了所有文件,使用了大约11%的CPU(从5%到16%)和不到40 MB的内存。

所以我的问题是:我应该如何进行?我可以限制线程的数量,使在同一类型下运行的线程不超过X个,但在我看来,这也证明了编程技能薄弱,因为不是每个CPU都能容纳相同数量的运行线程。那么我该怎么办呢?我真的需要从歌曲中提取这些细节——尽可能长时间,尽可能少的资源(了解它们的长度,以及它们是音频还是视频文件:在这里,我应该提到我的应用程序也播放电影,只是我不认为任何人需要在应用程序中同时加载数千部电影,如果我解决了音频问题,视频也会解决,因为歌曲只有在存储到列表中之前才能按电影区分,所以什么都没有这将与我问题的解决方案相冲突)。我真的需要帮助来解决这个问题。

编辑:我还附上了一些由ANTS性能档案显示的诊断:

  1. http://s24.postimg.org/e3e8cfcit/image1.png
  2. http://s24.postimg.org/71gaq88x1/image2.png

多线程的巨大内存和CPU使用量

你没有说你是如何启动线程的,但听起来像是在创建一个线程(即new Thread(...)并启动它。如果是这样的话,你会创建数百或数千个线程,每个线程都试图加载一首歌并验证它。这会导致一些严重的问题:

  1. 一次记住所有这些歌曲很可能会导致你的记忆力耗尽
  2. 在有数百个线程的情况下,计算机会花费大量时间进行线程上下文切换,让线程1运行一段时间,然后是线程2,然后是3,等等。很有可能你的计算机正在崩溃——花更多的时间进行线程上下文切换,而不是实际工作
  3. 从中加载文件的磁盘驱动器一次只能做一件事。如果两个线程请求加载一个文件,其中一个线程将不得不等待。因为读取文件可能比任何处理都要花费更长的时间,所以让多个线程来完成这项工作不太可能给你带来太多好处

你的设计太复杂了。您可以通过使用单个线程来简化它,减少内存需求,并可能提高处理速度。但如果一个线程慢两个,你可能只需要三个:

一个线程(主线程)获取文件名,检查它们,并将它们放在队列中。这是你清单上的第一步。

两个使用者线程读取队列并完成其余的处理。这些使用者线程中的每一个都在队列上等待并执行步骤4(加载文件、进行处理并将结果添加到列表中)。

使用BlockingCollection(一个并发队列),这类事情非常容易做到。基本思想是:

// this is the output list
List<MusicRecord> ProcessedRecords = new List<MusicRecord>();
object listLock = new object();  // object for locking the list when adding
// queue of file names to process
BlockingCollection<string> FilesToProcess = new BlockingCollection<string>();
// code for main thread
// Start your consumer threads here.
List<string> filesList = GetFilesListFromOpenDialog(); // however you do this
foreach (string fname in filesList)
{
    if (IsGoodFilename(fname))
    {
        string fullPath = CreateFullPath(fname);
        FilesToProcess.Add(fullPath); // add it to the files to be processed
    }
}
// no more files, mark the queue as complete for adding
// This marks the "end of the queue" so that clients reading the queue
// know when to stop.
FilesToProcess.CompleteAdding();
// here, wait for threads to complete

线程的代码非常简单:

foreach (var fname in FilesToProcess.GetConsumingEnumerable())
{
    // Load file and process it, creating a MusicRecord
    // Then add to output
    lock (listLock)
    {
        ProcessedRecord.Add(newRecord);
    }
}

这就是线程需要做的全部GetConsumingEnumerable处理队列上的等待(非繁忙)、取消项目排队以及在已知队列为空时退出。

有了这种设计,您可以从一个消费者线程开始,并根据需要扩展到任意数量。然而,拥有比CPU内核更多的线程是没有意义的,正如我之前所说,限制因素很可能是磁盘驱动器。

这是人们反复面临的线程常见问题之一。在正常情况下(在这种情况下为合理数量的文件),进程似乎运行良好。不幸的是,处理的文件越多,开销就越多——线程、句柄、MediaPlayer实例等——直到最终耗尽资源。

线程过多的另一个缺点是磁盘争用,这种情况早在系统资源耗尽之前就会发生。当你有很多线程试图从驱动器的不同部分读取时,硬盘驱动器将被迫花费更多的时间寻找不同的位置,而实际读取数据的时间则更少。更为复杂的是,运行的线程越多,驱动器为虚拟内存服务的时间就越多。

长话短说,使用数百或数千条线索是一个坏主意;。

与其为每个文件创建一个线程,不如使用已知大小的线程池(比如10个线程)并回收这些线程来完成工作。或者让线程从线程安全集合中提取自己的数据(我在Queue<T>类周围使用线程安全封装),并将结果发送到另一个线程安全集合,然后等待,直到准备好更多数据。

第4步对我来说没有意义。我建议你留在多个线程中加载文件和收集信息,也留在线程中排队文件和计算文件长度等。

我要做的改变是让一个消费者从播放列表队列中读取并实际播放文件,我不明白你为什么在这一节中使用线程。

如果使用单一消费者不起作用,请您对此进行详细说明,我们将尽力提供帮助。