突然的内存消耗跳跃导致在处理巨大的文本文件时导致内存不足异常

时间:2018-08-04 09:08:10

标签: c# .net stream out-of-memory task-parallel-library

我需要处理一个非常大的文本文件(6-8 GB)。我写了下面的代码。不幸的是,每次输出文件达到(在源文件旁边创建)达到〜2GB时,我发现内存消耗突然增加(从100MB到几GB),结果-内存不足

调试器指示OOM发生在while ((tempLine = streamReader.ReadLine()) != null) 我仅针对.NET 4.7和x64体系结构。 单行最长为50个字符。

我可以解决此问题,并将原始文件拆分为较小的部分,以免在处理时遇到问题,最后将结果合并到一个文件中,但不想这样做。

代码:

public async Task PerformDecodeAsync(string sourcePath, string targetPath)
    {
        var allLines = CountLines(sourcePath);
        long processedlines = default;
        using (File.Create(targetPath));
        var streamWriter = File.AppendText(targetPath);
        var decoderBlockingCollection = new BlockingCollection<string>(1000);
        var writerBlockingCollection = new BlockingCollection<string>(1000);

        var producer = Task.Factory.StartNew(() =>
        {
            using (var streamReader = new StreamReader(File.OpenRead(sourcePath), Encoding.Default, true))
            {
                string tempLine;
                while ((tempLine = streamReader.ReadLine()) != null)
                {
                    decoderBlockingCollection.Add(tempLine);
                }
                decoderBlockingCollection.CompleteAdding();

            }
        });
        var consumer1 = Task.Factory.StartNew(() =>
        {
            foreach (var line in decoderBlockingCollection.GetConsumingEnumerable())
            {
                short decodeCounter = 0;
                StringBuilder builder = new StringBuilder();
                foreach (var singleChar in line)
                {

                    var positionInDecodeKey = decodingKeysList[decodeCounter].IndexOf(singleChar);

                    if (positionInDecodeKey > 0)
                        builder.Append(model.Substring(positionInDecodeKey, 1));
                    else
                        builder.Append(singleChar);


                    if (decodeCounter > 18)
                        decodeCounter = 0;
                    else ++decodeCounter;
                }
                writerBlockingCollection.TryAdd(builder.ToString());
                Interlocked.Increment(ref processedlines);
                if (processedlines == (long)allLines)
                    writerBlockingCollection.CompleteAdding();
            }
        });
        var writer = Task.Factory.StartNew(() =>
        {
            foreach (var line in writerBlockingCollection.GetConsumingEnumerable())
            {
                streamWriter.WriteLine(line);
            }
        });
        Task.WaitAll(producer, consumer1, writer);
    }

非常感谢您提供解决方案以及如何对其进行优化的建议。

2 个答案:

答案 0 :(得分:0)

由于您所做的工作主要是与IO绑定,因此您实际上并没有从并行化中获得任何收益。在我看来,(如果我错了,请纠正我)您的转换算法不取决于您逐行读取文件,因此我建议改为执行以下操作:

void Main()
{
    //Setup streams for testing
    using(var inputStream = new MemoryStream())
    using(var outputStream = new MemoryStream())
    using (var inputWriter = new StreamWriter(inputStream))
    using (var outputReader = new StreamReader(outputStream))
    {
        //Write test string and rewind stream
        inputWriter.Write("abcdefghijklmnop");
        inputWriter.Flush();
        inputStream.Seek(0, SeekOrigin.Begin);

        var inputBuffer = new byte[5];
        var outputBuffer = new byte[5];
        int inputLength;
        while ((inputLength = inputStream.Read(inputBuffer, 0, inputBuffer.Length)) > 0)
        {
            for (var i = 0; i < inputLength; i++)
            {
                //transform each character
                outputBuffer[i] = ++inputBuffer[i];
            }

            //Write to output
            outputStream.Write(outputBuffer, 0, inputLength);
        }

        //Read for testing
        outputStream.Seek(0, SeekOrigin.Begin);
        var output = outputReader.ReadToEnd();
        Console.WriteLine(output);

        //Outputs: "bcdefghijklmnopq"
    }

}

很明显,您将使用FileStreams而不是MemoryStreams,并且可以将缓冲区长度增加到更大的长度(因为这只是一个示例)。同样,由于您的原始方法是Async,因此您将使用Stream.Write和Stream.Read

的异步变体。

答案 1 :(得分:0)

就像我说的那样,我可能会先做一些简单的事情,除非或直到证明它运行不佳为止。正如Adi在回答中所说,这项工作似乎受I / O约束-因此为它创建多个任务似乎没有什么好处。

publiv void PerformDecode(string sourcePath, string targetPath)
{
    File.WriteAllLines(targetPath,File.ReadLines(sourcePath).Select(line=>{
        short decodeCounter = 0;
        StringBuilder builder = new StringBuilder();
        foreach (var singleChar in line)
        {
            var positionInDecodeKey = decodingKeysList[decodeCounter].IndexOf(singleChar);
            if (positionInDecodeKey > 0)
                builder.Append(model.Substring(positionInDecodeKey, 1));
            else
                builder.Append(singleChar);

            if (decodeCounter > 18)
                decodeCounter = 0;
            else ++decodeCounter;
        }
        return builder.ToString();
    }));
}

现在,当然,这段代码实际上会阻塞直到完成,这就是为什么我没有将其标记为async的原因。但是,您的也是如此,它应该已经对此发出了警告。

(您可以尝试在Select部分使用PLINQ而不是LINQ,但老实说,我们在这里执行的处理的数量看起来微不足道;在应用任何此类更改之前,首先进行分析)