C# 使用System.Text.Json异步反序列化列表

C# 使用System.Text.Json异步反序列化列表,c#,.net-core,.net-core-3.0,c#-8.0,system.text.json,C#,.net Core,.net Core 3.0,C# 8.0,System.text.json,假设我请求一个包含许多对象列表的大型json文件。我不想让它们一次全部出现在内存中,但我宁愿一个接一个地阅读和处理它们。因此,我需要将异步System.IO.Stream流转换为IAsyncEnumerable。如何使用新的System.Text.JsonAPI来实现这一点 private async IAsyncEnumerable<T> GetList<T>(Uri url, CancellationToken cancellationToken = default)

假设我请求一个包含许多对象列表的大型json文件。我不想让它们一次全部出现在内存中,但我宁愿一个接一个地阅读和处理它们。因此,我需要将异步
System.IO.Stream
流转换为
IAsyncEnumerable
。如何使用新的
System.Text.Json
API来实现这一点

private async IAsyncEnumerable<T> GetList<T>(Uri url, CancellationToken cancellationToken = default)
{
    using (var httpResponse = await httpClient.GetAsync(url, cancellationToken))
    {
        using (var stream = await httpResponse.Content.ReadAsStreamAsync())
        {
            // Probably do something with JsonSerializer.DeserializeAsync here without serializing the entire thing in one go
        }
    }
}
私有异步IAsyncEnumerable GetList(Uri url,CancellationToken CancellationToken=default) { 使用(var httpResponse=await httpClient.GetAsync(url,cancellationToken)) { 使用(var stream=await httpResponse.Content.ReadAsStreamAsync()) { //可能在这里使用JsonSerializer.DeserializeAsync做一些事情,而不用一次性序列化整个内容 } } }
是的,在很多地方,一个真正的流式JSON(反)序列化程序将是一个很好的性能改进

不幸的是,
System.Text.Json
此时无法执行此操作。我不确定将来是否会这样——我希望如此!真正的JSON流式反序列化是相当具有挑战性的

您可以检查一下极快是否支持它

但是,可能会有一个针对您特定情况的定制解决方案,因为您的需求似乎限制了难度

其思想是一次从数组中手动读取一项。我们利用的事实是,列表中的每一项本身都是一个有效的JSON对象

您可以手动跳过
[
(对于第一项)或
(对于下一项)。然后,我认为您最好使用.NET Core的
Utf8JsonReader
来确定当前对象的结束位置,并将扫描的字节馈送到
JsonDeserializer

这样,一次只能对一个对象进行轻微缓冲


既然我们讨论的是性能,那么您可以在运行时从
管道读取器
获取输入。:-

也许您可以使用
Newtonsoft.Json
序列化程序?

特别是见第节:

优化内存使用

编辑

您可以尝试从JsonTextReader反序列化值,例如

using (var textReader = new StreamReader(stream))
using (var reader = new JsonTextReader(textReader))
{
    while (await reader.ReadAsync(cancellationToken))
    {
        yield return reader.Value;
    }
}

感觉上你需要实现你自己的流阅读器。你必须一个接一个地读取字节,并在对象定义完成后立即停止。这确实是非常低级别的。因此,你不会将整个文件加载到RAM中,而是将处理的部分加载到RAM中。这似乎是一个答案吗?

TL;DR这不是一件小事


看起来有人已经在寻找一个
Utf8JsonStreamReader
结构,该结构从流中读取缓冲区并将它们提供给Utf8JsonRreader,允许使用
JsonSerializer.Deserialize(ref newJsonReader,options);
。代码也不简单。相关的问题是,答案是

但这还不够-
HttpClient.GetAsync
只有在接收到整个响应后才会返回,本质上是缓冲内存中的所有内容

要避免这种情况,应与
HttpCompletionOption.ResponseHeadersRead
一起使用

反序列化循环也应该检查取消令牌,如果发出信号,则退出或抛出。否则循环将继续,直到接收并处理整个流

此代码基于相关答案的示例,使用
HttpCompletionOption.ResponseHeadersRead
并检查取消标记。它可以解析包含正确项目数组的JSON字符串,例如:

[{"prop1":123},{"prop1":234}]
jsonStreamReader.Read()
的第一个调用移动到数组的开头,而第二个调用移动到第一个对象的开头。当检测到数组的结尾(
]
)时,循环本身终止

private async IAsyncEnumerable<T> GetList<T>(Uri url, CancellationToken cancellationToken = default)
{
    //Don't cache the entire response
    using var httpResponse = await httpClient.GetAsync(url,                               
                                                       HttpCompletionOption.ResponseHeadersRead,  
                                                       cancellationToken);
    using var stream = await httpResponse.Content.ReadAsStreamAsync();
    using var jsonStreamReader = new Utf8JsonStreamReader(stream, 32 * 1024);

    jsonStreamReader.Read(); // move to array start
    jsonStreamReader.Read(); // move to start of the object

    while (jsonStreamReader.TokenType != JsonTokenType.EndArray)
    {
        //Gracefully return if cancellation is requested.
        //Could be cancellationToken.ThrowIfCancellationRequested()
        if(cancellationToken.IsCancellationRequested)
        {
            return;
        }

        // deserialize object
        var obj = jsonStreamReader.Deserialize<T>();
        yield return obj;

        // JsonSerializer.Deserialize ends on last token of the object parsed,
        // move to the first token of next object
        jsonStreamReader.Read();
    }
}
这不是一个有效的JSON文档,但单个片段是有效的。对于大数据/高度并发的场景,这有几个优点。添加新事件只需要在文件中添加新行,而不需要解析和重建整个文件。处理,尤其是并行处理更容易,原因有二:

  • 单个元素可以一次检索一个,只需从流中读取一行即可
  • 输入文件可以很容易地跨行边界进行分区和拆分,将每个部分提供给单独的辅助进程,例如在Hadoop集群中,或者在应用程序中提供不同的线程:通过将长度除以辅助进程的数量来计算拆分点,然后查找第一个换行。把所有东西都交给另一个工人
使用StreamReader

allocate-y方法是使用文本阅读器,一次读取一行,然后用以下代码解析:

方法从ReadOnlySequence读取项并返回结束位置,以便管道读取器可以从中恢复。不幸的是,我们想要返回IEnumerable或IAsyncEnumerable,迭代器方法也不喜欢
in
out
参数

我们可以在列表或队列中收集反序列化的项目,并将其作为单个结果返回,但这仍然会分配列表、缓冲区或节点,并且在返回之前必须等待缓冲区中的所有项目被反序列化:

private static (SequencePosition,List<T>) ReadItems(in ReadOnlySequence<byte> sequence, bool isCompleted)
DeserializeToChannel
方法在流的顶部创建一个管道读取器,创建一个通道并启动一个辅助任务,该任务解析块并将它们推送到通道:

ChannelReader<T> DeserializeToChannel<T>(Stream stream, CancellationToken token)
{
    var pipeReader = PipeReader.Create(stream);    
    var channel=Channel.CreateUnbounded<T>();
    var writer=channel.Writer;
    _ = Task.Run(async ()=>{
        while (!token.IsCancellationRequested)
        {
            var result = await pipeReader.ReadAsync(token); // read from the pipe

            var buffer = result.Buffer;

            var position = ReadItems(writer,buffer, result.IsCompleted,token); // read complete items from the current buffer

            if (result.IsCompleted) 
                break; // exit if we've read everything from the pipe

            pipeReader.AdvanceTo(position, buffer.End); //advance our position in the pipe
        }

        pipeReader.Complete(); 
    },token)
    .ContinueWith(t=>{
        pipeReader.Complete();
        writer.TryComplete(t.Exception);
    });

    return channel.Reader;
}

在.NET 5(C#9)中使用
System.IO.Pipelines
扩展包以及
System.Text.Json.JsonSerializer

使用系统;
使用系统缓冲区;
使用System.Collections.Generic;
使用System.IO;
使用系统IO管道;
使用系统文本;
使用System.Text.Json;
使用System.Threading.Tasks;
班级计划
{
private static SequencePosition ReadItems(in ReadOnlySequence<byte> sequence, bool isCompleted)
private static (SequencePosition,List<T>) ReadItems(in ReadOnlySequence<byte> sequence, bool isCompleted)
private const byte NL=(byte)'\n';
private const int MaxStackLength = 128;

private static SequencePosition ReadItems<T>(ChannelWriter<T> writer, in ReadOnlySequence<byte> sequence, 
                          bool isCompleted, CancellationToken token)
{
    var reader = new SequenceReader<byte>(sequence);

    while (!reader.End && !token.IsCancellationRequested) // loop until we've read the entire sequence
    {
        if (reader.TryReadTo(out ReadOnlySpan<byte> itemBytes, NL, advancePastDelimiter: true)) // we have an item to handle
        {
            var item=JsonSerializer.Deserialize<T>(itemBytes);
            writer.TryWrite(item);            
        }
        else if (isCompleted) // read last item which has no final delimiter
        {
            var item = ReadLastItem<T>(sequence.Slice(reader.Position));
            writer.TryWrite(item);
            reader.Advance(sequence.Length); // advance reader to the end
        }
        else // no more items in this sequence
        {
            break;
        }
    }

    return reader.Position;
}

private static T ReadLastItem<T>(in ReadOnlySequence<byte> sequence)
{
    var length = (int)sequence.Length;

    if (length < MaxStackLength) // if the item is small enough we'll stack allocate the buffer
    {
        Span<byte> byteBuffer = stackalloc byte[length];
        sequence.CopyTo(byteBuffer);
        var item=JsonSerializer.Deserialize<T>(byteBuffer);
        return item;        
    }
    else // otherwise we'll rent an array to use as the buffer
    {
        var byteBuffer = ArrayPool<byte>.Shared.Rent(length);

        try
        {
            sequence.CopyTo(byteBuffer);
            var item=JsonSerializer.Deserialize<T>(byteBuffer);
            return item;
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(byteBuffer);
        }

    }    
}
ChannelReader<T> DeserializeToChannel<T>(Stream stream, CancellationToken token)
{
    var pipeReader = PipeReader.Create(stream);    
    var channel=Channel.CreateUnbounded<T>();
    var writer=channel.Writer;
    _ = Task.Run(async ()=>{
        while (!token.IsCancellationRequested)
        {
            var result = await pipeReader.ReadAsync(token); // read from the pipe

            var buffer = result.Buffer;

            var position = ReadItems(writer,buffer, result.IsCompleted,token); // read complete items from the current buffer

            if (result.IsCompleted) 
                break; // exit if we've read everything from the pipe

            pipeReader.AdvanceTo(position, buffer.End); //advance our position in the pipe
        }

        pipeReader.Complete(); 
    },token)
    .ContinueWith(t=>{
        pipeReader.Complete();
        writer.TryComplete(t.Exception);
    });

    return channel.Reader;
}
var reader=DeserializeToChannel<MyEvent>(stream,cts.Token);
await foreach(var item in reader.ReadAllAsync(cts.Token))
{
    //Do something with it 
}    
private async IAsyncEnumerable<T> GetList<T>(Uri url, CancellationToken cancellationToken = default)
{
    using (var httpResponse = await httpClient.GetAsync(url, cancellationToken))
    {
        using (var stream = await httpResponse.Content.ReadAsStreamAsync())
        {

            await foreach(var item in JsonSerializer.DeserializeAsyncEnumerable<T>(stream))
            {
                yield return item;
            }
        }
    }
}