首页 > 解决方案 > 如何使用多线程优化大文件中单词和字符的计数?

问题描述

我有一个大约 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);
}

有什么方法可以使用线程使其更快,有人建议我使用线程以便更快?

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

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

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

标签: c#multithreadingoptimizationtext-filesstreamreader

解决方案


以下是如何使用并行性有效地解决对巨大文本文件的非空白字符进行计数的问题。首先,我们需要一种以流方式读取字符块的方法。本机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;
        }
    }
}

有了这个方法,就可以很容易地使用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();
}

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

使用Partitioner配置的 withNoBuffering以便每个线程一次获取一个块。没有它,PLINQ 将使用块分区,这意味着每个线程逐渐地一次请求越来越多的元素。在这种情况下,块分区是不合适的,因为仅仅枚举的行为是昂贵的。

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

blockSize您可能需要通过调整(越大越好)和MaxDegreeOfParallelism硬件功能来微调操作。Environment.ProcessorCount可能太多了,也2可能足够了。

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


推荐阅读