如何使用多线程优化大文件中单词和字符的计数?

viv*_*una 0 c# optimization multithreading streamreader text-files

我有一个大约 1 GB 的非常大的文本文件。

我需要计算单词和字符(非空格字符)的数量。

我写了下面的代码。

string fileName = "abc.txt";
long words = 0;
long characters = 0;
if (File.Exists(fileName))
{
    using (StreamReader sr = new StreamReader(fileName))
    {
        string[] fields = null;
        string text = sr.ReadToEnd();
        fields = text.Split(' ', StringSplitOptions.RemoveEmptyEntries);
        foreach (string str in fields)
        {
            characters += str.Length;
        }
        words += fields.LongLength;
    }

    Console.WriteLine("The word count is {0} and character count is {1}", words, characters);
}
Run Code Online (Sandbox Code Playgroud)

有没有办法使用线程使它更快,有人建议我使用线程以使其更快?

我在我的代码中发现了一个问题,如果单词或字符的数量大于long最大值,该问题就会失败。

我编写这段代码时假设只有英文字符,但也可以有非英文字符。

我特别在寻找与线程相关的建议。

The*_*ias 8

以下是如何使用并行性有效地解决对巨大文本文件的非空白字符进行计数的问题。首先,我们需要一种以流方式读取字符块的方法。本机File.ReadLines方法不会剪切它,因为文件容易只有一行。下面是一个方法,该StreamReader.ReadBlock方法使用该方法抓取特定大小的字符块,并将它们作为IEnumerable<char[]>.

public static IEnumerable<char[]> ReadCharBlocks(String path, int blockSize)
{
    using (var reader = new StreamReader(path))
    {
        while (true)
        {
            var block = new char[blockSize];
            var count = reader.ReadBlock(block, 0, block.Length);
            if (count == 0) break;
            if (count < block.Length) Array.Resize(ref block, count);
            yield return block;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

有了这种方法,就可以很容易地使用PLINQ并行化字符块的解析:

public static long GetNonWhiteSpaceCharsCount(string filePath)
{
    return Partitioner
        .Create(ReadCharBlocks(filePath, 10000), EnumerablePartitionerOptions.NoBuffering)
        .AsParallel()
        .WithDegreeOfParallelism(Environment.ProcessorCount)
        .Select(chars => chars
            .Where(c => !Char.IsWhiteSpace(c) && !Char.IsHighSurrogate(c))
            .LongCount())
        .Sum();
}
Run Code Online (Sandbox Code Playgroud)

上面发生的是多个线程正在读取文件并处理块,但读取文件是同步的。通过调用该IEnumerator<char[]>.MoveNext方法,一次只允许一个线程获取下一个块。这种行为不像纯粹的生产者-消费者设置,其中一个线程专用于读取文件,但实际上性能特征应该是相同的。这是因为此特定工作负载的可变性较低。解析每个字符块应该花费大约相同的时间。所以当一个线程读完一个块时,另一个线程应该在等待读取下一个块的列表中,导致组合读取操作几乎是连续的。

Partitioner被配置为与NoBuffering被使用,以便每个线程获取一次一个块。如果没有它,PLINQ 将使用块分区,这意味着每个线程一次会逐渐请求越来越多的元素。在这种情况下,块分区不适合,因为单纯的枚举行为代价高昂。

工作线程由ThreadPool. 当前线程也参与处理。所以在上面的例子中,假设当前线程是应用程序的主线程,那么提供的线程数ThreadPoolEnvironment.ProcessorCount - 1.

您可能需要通过调整blockSize(越大越好)和MaxDegreeOfParallelism硬件的功能来微调操作。在Environment.ProcessorCount可能是太多了,而且2很可能是不够的。

计算单词的问题要困难得多,因为一个单词可能跨越多个字符块。整个 1 GB 文件甚至可能包含一个单词。您可以尝试通过研究该方法的源代码来解决此问题,该StreamReader.ReadLine方法必须处理同类问题。提示:如果一个块以非空白字符结尾,而下一个块也以非空白字符开头,则肯定有一个单词被分成两半。您可以跟踪分成两半的单词数,并最终从单词总数中减去这个数字。