C# - OutOfMemoryException 在 JSON 文件中保存列表

Posted

技术标签:

【中文标题】C# - OutOfMemoryException 在 JSON 文件中保存列表【英文标题】:C# - OutOfMemoryException saving a List on a JSON file 【发布时间】:2019-02-18 01:12:34 【问题描述】:

我正在尝试保存压力图的流数据。 基本上我有一个压力矩阵定义为:

double[,] pressureMatrix = new double[e.Data.GetLength(0), e.Data.GetLength(1)];

基本上,我每 10 毫秒收到一个 pressureMatrix,我想将所有信息保存在 JSON 文件中,以便以后重现。

我要做的是,首先,写下我称之为标题的内容,其中包含用于进行录制的所有设置,如下所示:

recordedData.softwareVersion = Assembly.GetExecutingAssembly().GetName().Version.Major.ToString() + "." + Assembly.GetExecutingAssembly().GetName().Version.Minor.ToString();
recordedData.calibrationConfiguration = calibrationConfiguration;
recordedData.representationConfiguration = representationSettings;
recordedData.pressureData = new List<PressureMap>();

var json = JsonConvert.SerializeObject(csvRecordedData, Formatting.None);

File.WriteAllText(this.filePath, json);

然后,每次获得新的压力图时,我都会创建一个新线程来添加新的PressureMatrix 并重新写入文件:

var newPressureMatrix = new PressureMap(datos, DateTime.Now);
recordedData.pressureData.Add(newPressureMatrix);
var json = JsonConvert.SerializeObject(recordedData, Formatting.None);
File.WriteAllText(this.filePath, json);

大约 20-30 分钟后,我收到 OutOfMemory 异常,因为系统无法保存 recordedData 变量,因为其中的 List&lt;PressureMatrix&gt; 太大。

我该如何处理以保存数据?我想保存24-48小时的信息。

【问题讨论】:

几个想法:您要么需要处理小批量的项目,要么使用流式处理以增量方式转换和保存文件。 请注意,如果在序列化它之前不能保存内容,那么在反序列化它之后你很可能无法保存它(我猜你甚至无法反序列化它)。那么将这么多数据存储到一个 Json 中的目的是什么? 首先要尝试直接写入文件而不是中间string。您可能会达到effective string length limit - 实际上这很可能在达到List&lt;T&gt; 长度限制之前发生。见Performance Tips: Optimize Memory Usage 和Can Json.NET serialize / deserialize to / from a stream?。 将数百 MB 的数据写入磁盘非常慢!它每 10 毫秒发生一次!所以在接受Mayer的建议之前,最好先计算出PressureMap的大小,然后确定写操作的频率应该是多少(每10秒?每1分钟?) 接下来,您可能会考虑另一种文件格式,即Newline Delimited JSON。然后你不需要把所有的东西都保存在内存中,你可以把每个增量写成一个单独的 JSON 对象。请参阅:Serialize as NDJSON using Json.NET 和 Line delimited json serializing and de-serializing。 【参考方案1】:

您的基本问题是您将所有压力图样本都保存在内存中,而不是单独编写每个样本,然后将其作为垃圾收集。更糟糕的是,您在两个不同的地方这样做:

    在将字符串写入文件之前,您将整个样本列表序列化为 JSON 字符串 json

    相反,如Performance Tips: Optimize Memory Usage 中所述,在这种情况下,您应该直接对文件进行序列化和反序列化。有关如何执行此操作的说明,请参阅 this answer 至 Can Json.NET serialize / deserialize to / from a stream? 以及 Serialize JSON to a file

    recordedData.pressureData = new List&lt;PressureMap&gt;(); 会累积所有压力图样本,然后在每次生成样本时写入所有样本

    更好的解决方案是将每个样本编写一次并忘记它,但是每个样本都嵌套在 JSON 中的一些容器对象中的要求使得如何做到这一点并不明显。

那么,如何解决问题 #2?

首先,让我们修改你的数据模型如下,将标头数据划分到一个单独的类中:

public class PressureMap

    public double[,] PressureMatrix  get; set; 


public class CalibrationConfiguration 

    // Data model not included in question


public class RepresentationConfiguration 

    // Data model not included in question


public class RecordedDataHeader

    public string SoftwareVersion  get; set; 
    public CalibrationConfiguration CalibrationConfiguration  get; set; 
    public RepresentationConfiguration RepresentationConfiguration  get; set; 


public class RecordedData

    // Ensure the header is serialized first.
    [JsonProperty(Order = 1)]
    public RecordedDataHeader RecordedDataHeader  get; set; 
    // Ensure the pressure data is serialized last.
    [JsonProperty(Order = 2)]
    public IEnumerable<PressureMap> PressureData  get; set; 

选项 #1 是 producer-comsumer pattern 的一个版本。它涉及启动两个线程:一个用于生成PressureData 样本,另一个用于序列化RecordedData。第一个线程将生成样本并将它们添加到传递给第二个线程的BlockingCollection&lt;PressureMap&gt; 集合中。然后第二个线程将序列化BlockingCollection&lt;PressureMap&gt;.GetConsumingEnumerable() 作为RecordedData.PressureData 的值。

以下代码给出了如何做到这一点的框架:

var sampleCount = 400;    // Or whatever stopping criterion you prefer
var sampleInterval = 10;  // in ms

using (var pressureData = new BlockingCollection<PressureMap>())

    // Adapted from
    // https://docs.microsoft.com/en-us/dotnet/standard/collections/thread-safe/blockingcollection-overview
    // https://docs.microsoft.com/en-us/dotnet/api/system.collections.concurrent.blockingcollection-1?view=netframework-4.7.2

    // Spin up a Task to sample the pressure maps
    using (Task t1 = Task.Factory.StartNew(() =>
    
        for (int i = 0; i < sampleCount; i++)
        
            var data = GetPressureMap(i);
            Console.WriteLine("Generated sample 0", i);
            pressureData.Add(data);
            System.Threading.Thread.Sleep(sampleInterval);
        
        pressureData.CompleteAdding();
    ))
    
        // Spin up a Task to consume the BlockingCollection
        using (Task t2 = Task.Factory.StartNew(() =>
        
            var recordedDataHeader = new RecordedDataHeader
            
                SoftwareVersion = softwareVersion,
                CalibrationConfiguration = calibrationConfiguration,
                RepresentationConfiguration = representationConfiguration,
            ;

            var settings = new JsonSerializerSettings
            
                ContractResolver = new CamelCasePropertyNamesContractResolver(),
            ;

            using (var stream = new FileStream(this.filePath, FileMode.Create))
            using (var textWriter = new StreamWriter(stream))
            using (var jsonWriter = new JsonTextWriter(textWriter))
            
                int j = 0;

                var query = pressureData
                    .GetConsumingEnumerable()
                    .Select(p => 
                             
                                // Flush the writer periodically in case the process terminates abnormally
                                jsonWriter.Flush();
                                Console.WriteLine("Serializing item 0", j++);
                                return p;
                            );

                var recordedData = new RecordedData
                
                    RecordedDataHeader = recordedDataHeader,
                    // Since PressureData is declared as IEnumerable<PressureMap>, evaluation will be lazy.
                    PressureData = query,
                ;                          

                Console.WriteLine("Beginning serialization of 0 to 1:", recordedData, this.filePath);
                JsonSerializer.CreateDefault(settings).Serialize(textWriter, recordedData);
                Console.WriteLine("Finished serialization of 0 to 1.", recordedData, this.filePath);
            
        ))
        
            Task.WaitAll(t1, t2);
        
    

注意事项:

此解决方案使用的事实是,在序列化 IEnumerable&lt;T&gt; 时,Json.NET 不会将可枚举物化为列表。相反,它将充分利用惰性评估并简单地枚举它,写入然后忘记遇到的每个单独的项目。

第一个线程对PressureData 进行采样并将它们添加到阻塞集合中。

第二个线程将阻塞集合包装在 IEnumerable&lt;PressureData&gt; 中,然后将其序列化为 RecordedData.PressureData

在序列化期间,序列化程序将通过IEnumerable&lt;PressureData&gt; 枚举进行枚举,将每个流式传输到 JSON 文件,然后继续执行下一个 - 有效地阻塞,直到一个可用为止。

您需要做一些实验以确保序列化线程可以“跟上”采样线程,可能通过在构造期间设置BoundedCapacity。如果没有,您可能需要采用不同的策略。

PressureMap GetPressureMap(int count) 应该是您的某种方法(问题中未显示),它返回当前压力图样本。

在此技术中,JSON 文件在采样会话期间保持打开状态。如果采样异常终止,文件可能会被截断。我尝试通过定期刷新写入器来改善问题。

虽然数据序列化将不再需要无限量的内存,但稍后反序列化 RecordedData 会将 PressureData 数组反序列化为具体的 List&lt;PressureMap&gt;。这可能会导致下游处理期间出现内存问题。

演示小提琴 #1 here.

选项 #2 是从 JSON 文件切换到 Newline Delimited JSON 文件。这样的文件由由换行符分隔的 JSON 对象序列组成。在您的情况下,您将使第一个对象包含RecordedDataHeader 信息,后续对象的类型为PressureMap

var sampleCount = 100; // Or whatever
var sampleInterval = 10;

var recordedDataHeader = new RecordedDataHeader

    SoftwareVersion = softwareVersion,
    CalibrationConfiguration = calibrationConfiguration,
    RepresentationConfiguration = representationConfiguration,
;

var settings = new JsonSerializerSettings

    ContractResolver = new CamelCasePropertyNamesContractResolver(),
;

// Write the header
Console.WriteLine("Beginning serialization of sample data to 0.", this.filePath);

using (var stream = new FileStream(this.filePath, FileMode.Create))

    JsonExtensions.ToNewlineDelimitedJson(stream, new[]  recordedDataHeader );


// Write each sample incrementally

for (int i = 0; i < sampleCount; i++)

    Thread.Sleep(sampleInterval);
    Console.WriteLine("Performing sample 0 of 1", i, sampleCount);
    var map = GetPressureMap(i);

    using (var stream = new FileStream(this.filePath, FileMode.Append))
    
        JsonExtensions.ToNewlineDelimitedJson(stream, new[]  map );
    


Console.WriteLine("Finished serialization of sample data to 0.", this.filePath);

使用扩展方法:

public static partial class JsonExtensions

    // Adapted from the answer to
    // https://***.com/questions/44787652/serialize-as-ndjson-using-json-net
    // by dbc https://***.com/users/3744182/dbc
    public static void ToNewlineDelimitedJson<T>(Stream stream, IEnumerable<T> items)
    
        // Let caller dispose the underlying stream 
        using (var textWriter = new StreamWriter(stream, new UTF8Encoding(false, true), 1024, true))
        
            ToNewlineDelimitedJson(textWriter, items);
        
    

    public static void ToNewlineDelimitedJson<T>(TextWriter textWriter, IEnumerable<T> items)
    
        var serializer = JsonSerializer.CreateDefault();

        foreach (var item in items)
        
            // Formatting.None is the default; I set it here for clarity.
            using (var writer = new JsonTextWriter(textWriter)  Formatting = Formatting.None, CloseOutput = false )
            
                serializer.Serialize(writer, item);
            
            // http://specs.okfnlabs.org/ndjson/
            // Each JSON text MUST conform to the [RFC7159] standard and MUST be written to the stream followed by the newline character \n (0x0A). 
            // The newline charater MAY be preceeded by a carriage return \r (0x0D). The JSON texts MUST NOT contain newlines or carriage returns.
            textWriter.Write("\n");
        
    

    // Adapted from the answer to 
    // https://***.com/questions/29729063/line-delimited-json-serializing-and-de-serializing
    // by Yuval Itzchakov https://***.com/users/1870803/yuval-itzchakov
    public static IEnumerable<TBase> FromNewlineDelimitedJson<TBase, THeader, TRow>(TextReader reader)
        where THeader : TBase
        where TRow : TBase
    
        bool first = true;

        using (var jsonReader = new JsonTextReader(reader)  CloseInput = false, SupportMultipleContent = true )
        
            var serializer = JsonSerializer.CreateDefault();

            while (jsonReader.Read())
            
                if (jsonReader.TokenType == JsonToken.Comment)
                    continue;
                if (first)
                
                    yield return serializer.Deserialize<THeader>(jsonReader);
                    first = false;
                
                else
                
                    yield return serializer.Deserialize<TRow>(jsonReader);
                
            
        
    

稍后,您可以按如下方式处理换行符分隔的 JSON 文件:

using (var stream = File.OpenRead(filePath))
using (var textReader = new StreamReader(stream))

    foreach (var obj in JsonExtensions.FromNewlineDelimitedJson<object, RecordedDataHeader, PressureMap>(textReader))
    
        if (obj is RecordedDataHeader)
        
            var header = (RecordedDataHeader)obj;
            // Process the header
            Console.WriteLine(JsonConvert.SerializeObject(header));
        
        else
        
            var row = (PressureMap)obj;
            // Process the row.
            Console.WriteLine(JsonConvert.SerializeObject(row));
        
    

注意事项:

这种方法看起来更简单,因为样本是以增量方式添加到文件末尾的,而不是插入到某个整体 JSON 容器中。

使用这种方法,序列化和下游处理都可以通过有限的内存使用来完成。

示例文件在采样期间不会保持打开状态,因此不太可能被截断。

下游应用程序可能没有内置工具来处理以换行符分隔的 JSON。

此策略可能更简单地与您当前的线程代码集成。

演示小提琴#2 here.

【讨论】:

我在这篇冗长的文章中学到了一些新东西;)回答 JSON 可以直接序列化为文件。 这是解决此类问题的一种非常全面的方法。谢谢!

以上是关于C# - OutOfMemoryException 在 JSON 文件中保存列表的主要内容,如果未能解决你的问题,请参考以下文章

引发了“System.OutOfMemoryException”类型的异常。 C# 使用内存流时

C# - OutOfMemoryException 在 JSON 文件中保存列表

使用 ExcelLibrary 在 C# 中读取 Excel .xlsx 文件时出现 OutOfMemoryException

在 C# 中进行大量、快速和频繁的内存分配期间避免 OutOfMemoryException

C# 接收 java webservices接口返回的Byte[] 数组时,大于90M报错System.OutOfMemoryException 求高手解答

如何解决使用主成分分析引发的 OutOfMemoryException