如何使这些IO读取并行且具有性能

本文关键字:性能 并行 读取 何使这 IO | 更新日期: 2023-09-27 18:29:35

我有一个文件列表:基于C#的WPF应用程序中的List<string> Files

Files包含约1000000个唯一文件路径。

我在我的应用程序上运行了一个探查器。当我尝试进行并行操作时,它确实很滞后,因为它受IO限制。它甚至滞后于我的UI线程,尽管没有调度程序去处理它们(注意我标记的两行):

Files.AsParallel().ForAll(x =>
{
    char[] buffer = new char[0x100000];
    using (FileStream stream = new FileStream(x, FileMode.Open, FileAccess.Read)) // EXTREMELY SLOW
    using (StreamReader reader = new StreamReader(stream, true))
    {
        while (true)
        {
            int bytesRead = reader.Read(buffer, 0, buffer.Length); // EXTREMELY SLOW
            if (bytesRead <= 0)
            {
                break;
            }
        }
    }
}

这两行代码占据了我整个概要文件测试运行的70%。我想实现IO的最大并行化,同时保持性能,使其不会完全削弱我的应用程序的UI。没有其他事情影响我的表现。证明:使用Files.ForEach不会削弱我的UI,WithDegreeOfParallelism也有帮助(但是,我正在编写一个应该在任何PC上使用的应用程序,所以我不能为这个计算假设特定程度的并行性);另外,我使用的电脑有一个固态硬盘。我在StackOverflow上搜索过,找到了关于使用异步IO读取方法的链接。不过,我不确定它们在这种情况下是如何适用的。也许有人可以透露一些信息?而且如何调整新FileStream的构造函数时间;这可能吗?

编辑:嗯,我注意到了一些奇怪的事情。。。当我在使用AsParallel的同时将Read换成ReadAsync时,UI不会被压坏。简单地等待ReadAsync创建的任务完成会使我的UI线程保持一定程度的可用性。我认为这会进行某种异步调度,这种方法可以在不破坏现有线程的情况下保持最佳磁盘使用率。在这一点上,操作系统是否有可能争用现有线程来执行IO,比如我的应用程序的UI线程我真的不明白为什么它会减慢我的UI线程。操作系统调度是从我的线程上的IO还是其他什么?他们是否对CLR做了一些事情来吃掉那些没有使用Thread.BeginThreadAffinity或其他东西显式关联的线程?记忆不是问题;我在看《任务管理器》,里面有很多。

如何使这些IO读取并行且具有性能

我不同意你的说法,即你不能使用WithDegreeOfParallelism,因为它将在任何电脑上使用。你可以根据CPU的数量来确定。如果不使用WithDegreeOfParallelism,你会在一些电脑上崩溃。

您针对磁头不必移动的固态光盘进行了优化。我不认为这种不受限制的并行设计在普通光盘(任何电脑)上都适用。

我会尝试一个有3个队列的BlockingCollection:FileStream、StreamReader和ObservableCollection。将FileStream限制为4,它只需要领先于StreamReader即可。没有平行性。

一个头就是一个头。它从5或5000个文件中读取的速度不能比从1中读取的快。在固态光盘上,从一个文件切换到另一个文件不会受到惩罚——在普通光盘上,会受到很大的惩罚。如果你的文件是碎片化的,(在普通光盘上)会受到很大的惩罚。

您没有显示数据写入的内容,但下一步是将写入内容放入另一个队列,其中BlockingCollection中包含BlockingCollection。例如某人附加(正文);在单独的队列中。但这可能是超出其价值的开销。在一个连续的文件上保持接近100%的忙碌是你要做的最好的事情

private async Task<string> ReadTextAsync(string filePath)
{
    using (FileStream sourceStream = new FileStream(filePath,
        FileMode.Open, FileAccess.Read, FileShare.Read,
        bufferSize: 4096, useAsync: true))
    {
        StringBuilder sb = new StringBuilder();
        byte[] buffer = new byte[0x1000];
        int numRead;
        while ((numRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
        {
            string text = Encoding.Unicode.GetString(buffer, 0, numRead);
            sb.Append(text);
        }
        return sb.ToString();
    }
}

文件访问本质上是不并行的。只有在读取其他文件的同时处理某些文件,才能从并行性中获益。并行等待磁盘是没有意义的。

您的程序不是等待100000次1毫秒的磁盘访问,而是等待100000毫秒=100秒。

不幸的是,这是一个模糊的问题,没有可复制的代码示例。因此,不可能提供具体的建议。但我的两个建议是:

  • 传递一个ParallelOptions实例,在该实例中,您已将MaxDegreeOfParallelism属性设置为相当低的值。类似于系统中核心的数量,甚至是这个数字减去一。

  • 请确保您对磁盘的期望不会太高。您应该从磁盘和控制器的已知速度开始,并将其与您获得的数据吞吐量进行比较。如果您看起来已经达到或接近最大理论吞吐量,请将并行度调整得更低。

性能优化就是根据已知的硬件限制设定现实的目标,测量您的实际性能,然后研究如何改进算法中最昂贵的元素。如果你还没有完成前两步,你真的应该从那里开始。:)

我让它工作起来了;问题是我试图使用带有AddRange的ExtendedObservableCollection,而不是在每次UI调度中多次调用Add。。。出于某种原因,在我的情况下,人们在这里列出的方法的性能实际上较慢:ObservableCollection不;我不支持AddRange方法,所以我会收到每个添加项目的通知,此外INotifyCollectionChanging呢?

我认为,因为它迫使你用.Reset(reload)而不是.Add(diff)调用更改通知,所以存在某种导致瓶颈的逻辑。

我很抱歉没有发布代码的其余部分;我真的被这件事弄糊涂了,我稍后会解释原因。此外,对于遇到同样问题的其他人,这可能会有所帮助。在这种情况下,评测工具的主要问题是它们在这里没有多大帮助。不管怎样,应用程序的大部分时间都将用于读取文件。因此,您必须分别对所有调度器进行单元测试。