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

时间:2013-08-08 21:04:42

标签: c# multithreading memory-management media-player threadpool

我正在尝试创建一个具有播放列表选项的媒体播放器。当一个负载10-20首歌没有问题。所以我尝试了更苛刻的东西:我试图加载2048首歌曲(我拍了好几首歌并复制了很多次)。试图将它们加载到我的媒体播放器中,我的CPU和Ram内存增长了95%以上(只加载了前250首歌曲),有一次我的电脑重启了。所以我试图通过使用不让应用程序接管计算机的东西来减慢操作:如果CPU负载超过85%并且内存负载超过90%(我使用64位),我会停止加载新歌曲操作系统与Windows 8,如果这很重要)。它在某种程度上起作用,允许我加载近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,如果文件有音频,文件是否有视频并记住这3个变量与列表中文件的路径。
  5. 还有另一个线程可以验证是否有线程已完成其工作并已将媒体文件添加到List中,如果有,则删除对它们的引用,以便垃圾收集器将处理它们
  6. “mscorlib.dll中出现'System.OutOfMemoryException'类型未处理的异常”出现在下一行:

    MediaCreator[idx].CreatorThread.Start();
    

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

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

    所以我的问题是:我该怎么办?我可以限制线程数,以便不超过X个线程以相同的类型运行,但这似乎也证明了编程技巧不强,因为不是每个CPU都可以保持相同数量的运行线程。所以我该怎么做 ?我真的需要从歌曲中获取这些细节 - 尽可能用尽可能少的资源(知道它们有多长,如果它们是音频或视频文件:这里我应该提到我的应用程序也播放电影,它是只是因为我不认为任何人需要在应用程序中同时加载数千部电影,如果我解决了音频问题,它也将解决视频,因为歌曲只有在电影存储到列表之前才被电影区分开来 - 所以没有什么可以解决我的问题的解决方案)。我真的需要帮助才能解决这个问题。

    编辑:我还附上了ANTS Performance Profiles显示的一些诊断信息:

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

3 个答案:

答案 0 :(得分:5)

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

  1. 一次将所有这些歌曲留在内存中很可能会导致内存不足。
  2. 有数百个线程,计算机花费大量时间进行线程上下文切换,让线程1运行一点,然后线程2,然后3,等等。很可能你的计算机正在颠簸 - 花更多的时间做线程上下文切换比做实际工作。
  3. 您从中加载文件的磁盘驱动器一次只能执行一项操作。如果两个线程要求加载文件,其中一个将不得不等待。因为读取文件可能比任何处理花费的时间更长,所以让多个线程完成这项工作的可能性不大。
  4. 你的设计严重过于复杂。您可以通过使用单个线程来简化它,减少内存需求,并可能提高处理速度。但是如果一个线程慢两个,你可能想要的不超过三个:

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

    两个使用者线程读取队列并执行剩余的处理。这些消费者线程中的每一个都在队列上等待并执行步骤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内核更多的线程是没有意义的,正如我之前所说的那样,限制因素很可能就是你的磁盘驱动器。

答案 1 :(得分:1)

这是人们一遍又一遍地面对线程的常见问题之一。在正常情况下(在这种情况下合理数量的文件),进程出现以正常工作。不幸的是,处理的文件越多,开销越多 - 线程,句柄,MediaPlayer实例等 - 直到最终耗尽资源。

太多线程的另一个缺点是,在系统资源耗尽之前很久就会发生磁盘争用。当你有很多线程试图从驱动器的不同部分读取时,硬盘驱动器将被迫花更多的时间寻找不同的位置,而实际读取数据的时间更少。为了进一步加剧问题,运行的线程越多,驱动器用于维护虚拟内存的时间就越长。

长话短说,使用数百或数千个线程是A Bad Idea™。

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

答案 2 :(得分:0)

第4步以后对我没有意义。我建议你继续使用多个线程来加载文件并收集信息,并保留排队文件和计算文件长度等的线程。

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

如果使用单个消费者不起作用,请您继续展开,我们会尽力帮助。

相关问题