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

C# 如何使用多线程优化大型文件中的单词和字符计数?,c#,multithreading,optimization,text-files,streamreader,C#,Multithreading,Optimization,Text Files,Streamreader,我有一个非常大的文本文件,大约1GB 我需要计算单词和字符的数量(非空格字符) 我已经写了下面的代码 string fileName = "abc.txt"; long words = 0; long characters = 0; if (File.Exists(fileName)) { using (StreamReader sr = new StreamReader(fileName)) { string[] fields = null;

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

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

我已经写了下面的代码

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
max值,则该问题将失败

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


我特别希望得到与线程相关的建议。

这是一个根本不需要多线程处理的问题!为什么?因为CPU比磁盘IO快得多!因此,即使在单线程应用程序中,程序也将等待从磁盘读取数据。使用更多的线程意味着更多的等待。 您需要的是异步文件IO。这样的设计:-

main
  asynchronously read a chunk of the file (one MB perhaps), calling the callback on completion
  while not at end of file
    wait for asynchronous read to complete
    process chunk of data
  end
end

asynchronous read completion callback
  flag data available to process
  asynchronously read next chunk of the file, calling the callback on completion
end

下面是如何使用并行性有效地解决计算大型文本文件中非空白字符的问题。首先,我们需要一种以流式方式读取字符块的方法。本机方法不会剪切该文件,因为该文件可能只有一行。下面是一个方法,它使用该方法获取特定大小的字符块,并将它们作为
IEnumerable
返回

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

使用配置为的
分区器
,以便每个线程一次获取一个块。如果没有它,PLINQ将利用块分区,这意味着每个线程一次渐进地请求越来越多的元素。在这种情况下,块分区并不合适,因为仅仅枚举的行为代价很高

辅助线程由提供。当前线程也参与处理。因此,在上面的示例中,假设当前线程是应用程序的主线程,
ThreadPool
提供的线程数是
Environment.ProcessorCount-1

您可能需要根据硬件的功能调整
块大小(越大越好)和
MaxDegreeOfParallelism
来微调操作。
Environment.ProcessorCount
可能太多,
2
可能就足够了


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

您可以从开头获得文件的长度。设为“S”(字节)。 然后,让我们取一些常数“C”

执行C线程,让每个线程处理S/C长度的文本。 您可以一次读取所有文件并加载到内存中(如果您有足够的RAM),或者让每个线程读取文件的相关部分

第一个线程将处理字节0到S/C。 第二个线程将处理字节S/C到2S/C。 等等

所有线程完成后,汇总计数


怎么样?

@OlivierRogier是的,我也试过逐行阅读。但是,假设整个文件中只有一行,如果只有一行,最好是读取适当长度的字节块,然后一次处理一行。“编辑:我特别想寻找与线程相关的建议”->不,你没有。多个线程读取同一文件只会导致并发问题,并引发异常或读取无效数据。不要相信别人告诉你的任何事情,先学习。在这里使用多线程的唯一方法是将文件拆分成足够多的不同文件,这意味着将数据读写回磁盘,因此不会有任何改进。您可以读取多个块并在内存中同时处理它们,但增加的复杂性几乎肯定会超过任何(可能很小的)性能好处,因为这些好处非常广泛,而且编程需要几个小时。我会给你一些基本知识。您可以使用
Read
读取字节块,从中获取适当的字符,并可能将其发送到类似TPL数据流块的地方,这个答案可能依赖于异步IO总是更快的假设。这并非普遍正确。有没有办法在读取完成时调用该方法?@mjwills:是的,异步读取有额外的设置时间,但它是一个大文件,因此与正在进行的所有其他操作相比,开销应该可以忽略不计。对于小文件,您是对的,这将是一个问题。您没有理由不能处理回调中的数据块,只要确保在处理刚刚读取的数据之前开始下一次读取。是否可能
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;
        }
    }
}
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();
}