如何在多个线程中并行处理文件中的数据,并将它们写入另一个文件,同时保持原始数据顺序(C#)

如何在多个线程中并行处理文件中的数据,并将它们写入另一个文件,同时保持原始数据顺序(C#),c#,multithreading,queue,producer-consumer,C#,Multithreading,Queue,Producer Consumer,我想问你一个比较笼统的问题(尽管我对如何在C#中实现它很感兴趣) 我有一个巨大的文件,我想逐块读取,在几个线程中以某种方式并行处理这些块,以加快处理速度,然后按照读取原始数据块的相同顺序将处理后的数据写入另一个文件(即,确保从输入文件读取的第一个数据块将首先被处理并保存在输出文件中,第二个数据块将被处理并作为第二个数据块保存到输出文件中,等等) 我在考虑以某种方式实现生产者-消费者(producer-consumer)(即,通过块连续读取原始文件-提供一些队列,一组线程将从中读取和处理数据)但我

我想问你一个比较笼统的问题(尽管我对如何在C#中实现它很感兴趣)

我有一个巨大的文件,我想逐块读取,在几个线程中以某种方式并行处理这些块,以加快处理速度,然后按照读取原始数据块的相同顺序将处理后的数据写入另一个文件(即,确保从输入文件读取的第一个数据块将首先被处理并保存在输出文件中,第二个数据块将被处理并作为第二个数据块保存到输出文件中,等等)

我在考虑以某种方式实现生产者-消费者(producer-consumer)(即,通过块连续读取原始文件-提供一些队列,一组线程将从中读取和处理数据)但我不知道如何将处理后的数据从这些线程写入输出文件,以保持数据的顺序。即使我尝试将线程生成的处理后的数据块放入另一个队列,从该队列中它们可以被消费并写入输出文件,我仍然无法控制从该队列返回的数据的顺序线程(从而以正确的顺序将它们写入输出文件)

有什么建议吗


我对这方面还不熟悉,所以即使是理论上的提示对我来说也意义重大。

尽管这个问题有点开放,没有显示任何代码

解决这个问题有多种方法,它们都完全取决于您的需求和限制

尽管首先也是最重要的是,如果您试图解决的瓶颈是IO,那么并行任何东西都可能不会有帮助

然而,如果您需要在并行处理CPU绑定的工作之后保持顺序,那么有各种各样的TPL方法可以保持顺序,例如

  • PLinq哪有
  • 具有与并行选项的TPL数据流块
  • 你也可以使用反应式扩展(RX),我相信它也有类似的功能
最简单的方法(假设数据不能在离散块中读取和写入)是同步读取文件块(缓冲区),与“确保有序”功能并行处理数据,然后分批回写文件。显然,您必须处理读写的文件数据量(缓冲区大小)以查看什么适合您的情况


值得注意的是,您可以实现读/写
异步IO
,但它可能依赖于固定大小的记录(互斥)文件的结构。

这里有一种方法,您可以使用并行性将文件分块处理,并按照原始顺序将处理后的分块写入另一个文件中。此方法使用库作为包提供。如果您使用.NET Core,则无需安装此包,因为此pla中嵌入了TPL数据流另一个依赖项是包,其中包含用于将文件行分块的方法

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
//...

public static async Task ProcessFile(string sourcePath, Encoding sourceEncoding,
    string targetPath, Encoding targetEncoding,
    Func<string, string> lineTransformation,
    int degreeOfParallelism, int chunkSize)
{
    using StreamWriter writer = new StreamWriter(targetPath, false, targetEncoding);
    var cts = new CancellationTokenSource();

    var processingBlock = new TransformBlock<IList<string>, IList<string>>(chunk =>
    {
        return chunk.Select(line => lineTransformation(line)).ToArray();
    }, new ExecutionDataflowBlockOptions()
    {
        MaxDegreeOfParallelism = degreeOfParallelism,
        BoundedCapacity = 100, // prevent excessive buffering
        EnsureOrdered = true, // this is the default, but lets be explicit
        CancellationToken = cts.Token, // have a way to abort the processing
    });

    var writerBlock = new ActionBlock<IList<string>>(chunk =>
    {
        foreach (var line in chunk)
        {
            writer.WriteLine(line);
        }
    }); // The default options are OK for this block

    // Link the blocks and propagate completion
    processingBlock.LinkTo(writerBlock,
        new DataflowLinkOptions() { PropagateCompletion = true });

    // In case the writer block fails, the processing block must be canceled
    OnFaultedCancel(writerBlock, cts);

    static async void OnFaultedCancel(IDataflowBlock block, CancellationTokenSource cts)
    {
        try
        {
            await block.Completion.ConfigureAwait(false);
        }
        catch
        {
            cts.Cancel();
        }
    }

    // Feed the processing block with chunks from the source file
    await Task.Run(async () =>
    {
        try
        {
            var chunks = File.ReadLines(sourcePath, sourceEncoding)
                .Buffer(chunkSize);
            foreach (var chunk in chunks)
            {
                var sent = await processingBlock.SendAsync(chunk, cts.Token)
                    .ConfigureAwait(false);
                if (!sent) break; // Happens in case of a processing failure
            }
            processingBlock.Complete();
        }
        catch (OperationCanceledException)
        {
            processingBlock.Complete(); // Cancellation is not an error
        }
        catch (Exception ex)
        {
            // Reading error
            // Propagate by completing the processing block in a faulted state
            ((IDataflowBlock)processingBlock).Fault(ex);
        }
    }).ConfigureAwait(false);

    // All possible exceptions have been propagated to the writer block
    await writerBlock.Completion.ConfigureAwait(false);
}

ProcessFile
方法的一个已知缺陷是,它通过
任务假装异步。围绕同步方法运行
。不幸的是,目前没有有效的内置方法异步读取.NET Framework或.NET Core中的文本文件行。

您应该使用Microsoft的反应式框架(aka Rx)-numget
System.Reactive
并使用System.Reactive.Linq;
添加
-然后您可以执行以下操作:

IDisposable subscription =
    File
        .ReadLines("Huge File.txt")
        .ToObservable()
        .Buffer(200)
        .Select((lines, index) => new { lines, index })
        .SelectMany(lis => Observable.Start(() => new { lis.index, output = ProcessChunk(lis.lines) }))
        .ToArray()
        .Select(xs => xs.OrderBy(x => x.index).SelectMany(x => x.output))
        .Subscribe(xs =>
        {
            File.WriteAllLines("Output File.txt", xs.ToArray());
        });
那就是一次并行处理200行


请记住,IO比CPU处理慢得多,因此除非
ProcessChunk
非常占用CPU,否则任何多线程方法都可能无法提高性能-事实上可能会降低性能。

不确定是否可以多线程方式写入同一文件输出块的大小是否与inp相同ut块?在这种情况下,您可以使用Stream.Seek()编写定位文件。如果您可以预先计算输出块大小,则同样可以计算输出块在目标文件中的特征位置。对于RX解决方案,始终为+1。这要求在将所有数据写入光盘之前将其存储在内存中。我们不知道文件有多大,但它可能足够大,无法放入可用文件中标记机器的内存。你如何用这种方法控制并行度?@TheodorZoulias-内存是正确的-然而,任何需要将部件重新排序的多线程方法都存在这个问题。虽然可以编写一个查询来完成这项工作,但最坏的情况下仍然需要它“全部在内存中”。@TheodorZoulias-并行很容易。使用
SelectMany(…)
而不是
Select(…).Merge(degreeOfParallelism)
。TPL数据流通过包含该选项解决了过度缓冲的问题。在最坏的情况下,并行度将降低,而不是进程因
OutOfMemoryException
而崩溃。RX解决方案的另一个反复出现的问题是
等待
完成pro特许经营不是开箱即用的。每次你都需要即兴发挥来实现这个琐碎的目标。
IDisposable subscription =
    File
        .ReadLines("Huge File.txt")
        .ToObservable()
        .Buffer(200)
        .Select((lines, index) => new { lines, index })
        .SelectMany(lis => Observable.Start(() => new { lis.index, output = ProcessChunk(lis.lines) }))
        .ToArray()
        .Select(xs => xs.OrderBy(x => x.index).SelectMany(x => x.output))
        .Subscribe(xs =>
        {
            File.WriteAllLines("Output File.txt", xs.ToArray());
        });