1

我正在尝试创建一个具有播放列表选项的媒体播放器。当加载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 类中加载歌曲并验证接下来的事情:文件的时间跨度,文件是否有音频,文件是否有视频,并记住这 3 个变量以及路径列表中的文件。
  5. 还有另一个线程验证是否有线程已完成其工作并将媒体文件添加到列表中,如果有,则删除对它们的引用,以便垃圾收集器处理它们。

“在 mscorlib.dll 中发生类型为 'System.OutOfMemoryException' 的未处理异常”出现在下一行:

MediaCreator[idx].CreatorThread.Start();

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

我还想提一下,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
4

3 回答 3

5

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

  1. 一次将所有这些歌曲都放在内存中很可能会导致您的内存不足。
  2. 由于有数百个线程,计算机会花费大量时间进行线程上下文切换,让线程 1 运行一段时间,然后运行线程 2,然后运行 ​​3,等等。您的计算机很可能正在颠簸——花费更多时间来处理线程上下文切换而不是做实际工作。
  3. 您从中加载文件的磁盘驱动器一次只能做一件事。如果两个线程要求加载一个文件,其中一个将不得不等待。因为读取文件可能比任何处理都需要更长的时间,所以让多个线程执行这项工作不太可能让您受益匪浅。

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

一个线程(主线程)获取文件名,检查它们,然后将它们放入队列中。这是您清单上的第 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 内核更多的线程是没有意义的,正如我之前所说的那样,限制因素很可能是您的磁盘驱动器。

于 2013-08-08T21:43:30.847 回答
1

这是人们一遍又一遍地面临的线程常见问题之一。在正常情况下(在这种情况下是合理数量的文件),该过程似乎可以正常工作。不幸的是,你处理的文件越多,开销就越大——线程、句柄、MediaPlayer实例等——直到最终你耗尽资源。

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

长话短说,使用数百或数千个线程是一个坏主意™。

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

于 2013-08-09T04:43:05.377 回答
0

第 4 步以后对我来说没有意义。我建议您使用多个线程进行文件加载和收集信息,并使用线程排队文件和计算文件长度等。

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

如果使用单个消费者不起作用,请您对此进行扩展,我们会尽力提供帮助。

于 2013-08-08T21:14:56.873 回答