Phân tích lại một cách không chính xác một danh sách bằng System.Text.Json


11

Hãy nói rằng tôi yêu cầu một tệp json lớn chứa danh sách nhiều đối tượng. Tôi không muốn chúng ở trong bộ nhớ cùng một lúc, nhưng tôi thà đọc và xử lý từng cái một. Vì vậy, tôi cần phải biến một System.IO.Streamluồng không đồng bộ thành một IAsyncEnumerable<T>. Làm cách nào để sử dụng System.Text.JsonAPI mới để thực hiện việc này?

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
        }
    }
}

1
Bạn có thể sẽ cần một cái gì đó như phương pháp DeserializeAsync
Pavel Anikhouski

2
Xin lỗi, có vẻ như phương thức trên tải toàn bộ luồng vào bộ nhớ. Bạn có thể đọc dữ liệu bằng khối asynchonously sử dụng Utf8JsonReader, xin vui lòng có một cái nhìn tại một số github mẫu và tại hiện chủ đề cũng
Pavel Anikhouski

GetAsynctự trả về khi nhận được toàn bộ phản hồi. SendAsyncThay vào đó, bạn cần sử dụng với `httpCompletionOption.ResponseContentRead`. Khi bạn đã có điều đó, bạn có thể sử dụng JsonTextReader của JSON.NET . Sử dụng System.Text.Jsoncho điều này không phải là dễ dàng như vấn đề này cho thấy . Các chức năng không có sẵn và thực hiện nó trong phân bổ thấp bằng cách sử dụng các cấu trúc không tầm thường
Panagiotis Kanavos

Vấn đề với quá trình khử muối trong khối là bạn phải biết khi nào bạn có một đoạn hoàn chỉnh để khử lưu huỳnh. Điều này sẽ khó thực hiện sạch cho các trường hợp chung. Nó sẽ yêu cầu phân tích cú pháp trước, có thể là một sự đánh đổi khá kém về mặt hiệu suất. Nó khá khó để khái quát. Nhưng nếu bạn thực thi các hạn chế của riêng mình đối với JSON của mình, hãy nói rằng "một đối tượng duy nhất chiếm chính xác 20 dòng trong tệp", về cơ bản bạn có thể giải tuần tự hóa không đồng bộ bằng cách đọc tệp trong các khối không đồng bộ. Bạn sẽ cần một json lớn để thấy lợi ích ở đây, tôi sẽ tưởng tượng.
Thám

Có vẻ như ai đó đã trả lời một câu hỏi tương tự ở đây với mã đầy đủ.
Panagiotis Kanavos

Câu trả lời:


4

Có, một serializer JSON (de) thực sự phát trực tuyến sẽ là một cải tiến hiệu suất tốt để có, ở rất nhiều nơi.

Thật không may, System.Text.Jsonkhông làm điều này tại thời điểm này. Tôi không chắc nó sẽ trong tương lai - tôi hy vọng vậy! Quá trình khử lưu lượng thực sự của JSON hóa ra khá khó khăn.

Bạn có thể kiểm tra xem Utf8Json có hỗ trợ cực nhanh không, có lẽ.

Tuy nhiên, có thể có một giải pháp tùy chỉnh cho tình huống cụ thể của bạn, vì các yêu cầu của bạn dường như hạn chế khó khăn.

Ý tưởng là đọc thủ công một mục từ mảng tại một thời điểm. Chúng tôi đang sử dụng thực tế là mỗi mục trong danh sách là một đối tượng JSON hợp lệ.

Bạn có thể bỏ qua thủ công [(đối với mục đầu tiên) hoặc ,(đối với từng mục tiếp theo). Sau đó, tôi nghĩ rằng cách tốt nhất của bạn là sử dụng .NET Core Utf8JsonReaderđể xác định nơi đối tượng hiện tại kết thúc và đưa các byte được quét vào JsonDeserializer.

Bằng cách này, bạn chỉ đệm một chút trên một đối tượng tại một thời điểm.

Và vì chúng ta đang nói về hiệu suất, bạn có thể nhận được đầu vào từ một PipeReader, trong khi bạn đang ở đó. :-)


Đây không phải là về hiệu suất cả. Nó không phải là về khử lưu động không đồng bộ, mà nó đã làm. Đó là về truy cập phát trực tuyến - xử lý các phần tử JSON khi chúng được phân tích cú pháp từ luồng, theo cách JsonTextReader của JSON.NET.
Panagiotis Kanavos

Lớp có liên quan trong Utf8Json là JsonReader và như tác giả nói, thật kỳ lạ. JsonTextReader và System.Text của JSON.NET. Utf8JsonReader có cùng một sự kỳ lạ - bạn phải lặp và kiểm tra loại phần tử hiện tại khi bạn đi.
Panagiotis Kanavos

@PanagiotisKanavos À, vâng, phát trực tuyến. Đó là từ tôi đang tìm kiếm! Tôi đang cập nhật từ "không đồng bộ" thành "phát trực tuyến". Tôi tin rằng lý do muốn phát trực tuyến là hạn chế sử dụng bộ nhớ, đây là một vấn đề hiệu suất. Có lẽ OP có thể xác nhận.
Timo

Hiệu suất không có nghĩa là tốc độ. Cho dù trình giải nén nhanh đến mức nào, nếu bạn phải xử lý các mục 1M, bạn không muốn lưu trữ chúng trong RAM, cũng không phải chờ tất cả chúng được giải nén trước khi bạn có thể xử lý mục đầu tiên.
Panagiotis Kanavos

Ngữ nghĩa, bạn của tôi! Tôi rất vui vì chúng tôi đang cố gắng để đạt được điều tương tự sau khi tất cả.
Timo

4

TL; DR Nó không tầm thường


Có vẻ như ai đó đã đăng mã đầy đủ cho một Utf8JsonStreamReadercấu trúc đọc bộ đệm từ luồng và đưa chúng vào Utf8JsonRreader, cho phép dễ dàng giải nén JsonSerializer.Deserialize<T>(ref newJsonReader, options);. Mã này cũng không tầm thường. Câu hỏi liên quan là ở đây và câu trả lời là ở đây .

Điều đó vẫn chưa đủ - HttpClient.GetAsyncsẽ chỉ trở lại sau khi nhận được toàn bộ phản hồi, về cơ bản là đệm mọi thứ trong bộ nhớ.

Để tránh điều này, nên sử dụng httpClient.GetAsync (chuỗi, httpCompletionOption)HttpCompletionOption.ResponseHeadersRead .

Vòng lặp khử lưu huỳnh cũng nên kiểm tra mã thông báo hủy và thoát hoặc ném nếu nó được báo hiệu. Nếu không, vòng lặp sẽ tiếp tục cho đến khi toàn bộ luồng được nhận và xử lý.

Mã này dựa trên ví dụ của câu trả lời liên quan và sử dụng HttpCompletionOption.ResponseHeadersReadvà kiểm tra mã thông báo hủy. Nó có thể phân tích các chuỗi JSON có chứa một mảng các mục thích hợp, ví dụ:

[{"prop1":123},{"prop1":234}]

Cuộc gọi đầu tiên jsonStreamReader.Read()di chuyển đến điểm bắt đầu của mảng trong khi cuộc gọi thứ hai di chuyển đến điểm bắt đầu của đối tượng đầu tiên. Vòng lặp tự chấm dứt khi kết thúc mảng ( ]) được phát hiện.

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();
    }
}

Các đoạn JSON, AKA phát trực tuyến JSON aka ... *

Nó khá phổ biến trong các tình huống phát trực tuyến hoặc ghi nhật ký để nối các đối tượng JSON riêng lẻ vào một tệp, một phần tử trên mỗi dòng, ví dụ:

{"eventId":1}
{"eventId":2}
...
{"eventId":1234567}

Đây không phải là một tài liệu JSON hợp lệ nhưng các đoạn riêng lẻ là hợp lệ. Điều này có một số lợi thế cho dữ liệu lớn / kịch bản đồng thời cao. Thêm một sự kiện mới chỉ yêu cầu nối thêm một dòng mới vào tệp, không phân tích và xây dựng lại toàn bộ tệp. Xử lý , đặc biệt là xử lý song song dễ dàng hơn vì hai lý do:

  • Các yếu tố riêng lẻ có thể được truy xuất từng cái một, chỉ bằng cách đọc một dòng từ một luồng.
  • Tệp đầu vào có thể dễ dàng phân vùng và phân chia theo ranh giới dòng, cung cấp từng phần cho một quy trình công nhân riêng biệt, ví dụ như trong cụm Hadoop hoặc đơn giản là các luồng khác nhau trong một ứng dụng: Tính các điểm phân chia, ví dụ bằng cách chia độ dài cho số lượng công nhân , sau đó tìm kiếm dòng mới đầu tiên. Cho ăn tất cả mọi thứ cho đến thời điểm đó cho một công nhân riêng biệt.

Sử dụng StreamReader

Cách phân bổ để làm điều này sẽ là sử dụng TextReader, đọc từng dòng một và phân tích cú pháp với JsonSerializer.Deserialize :

using var reader=new StreamReader(stream);
string line;
//ReadLineAsync() doesn't accept a CancellationToken 
while((line=await reader.ReadLineAsync()) != null)
{
    var item=JsonSerializer.Deserialize<T>(line);
    yield return item;

    if(cancellationToken.IsCancellationRequested)
    {
        return;
    }
}

Điều đó đơn giản hơn nhiều so với mã giải mã một mảng thích hợp. Có hai vấn đề:

  • ReadLineAsync không chấp nhận mã thông báo hủy
  • Mỗi lần lặp phân bổ một chuỗi mới, một trong những điều chúng tôi muốn tránh bằng cách sử dụng System.Text.Json

Điều này có thể là đủ mặc dù khi cố gắng tạo ra các ReadOnlySpan<Byte>bộ đệm cần thiết bởi JsonSerializer.Deserialize không tầm thường.

Đường ống và trình tự

Để tránh sự phân bổ, chúng ta cần lấy một ReadOnlySpan<byte>luồng từ luồng. Làm điều này đòi hỏi phải sử dụng các ống System.IO.Pipeline và cấu trúc SequenceReader . Giới thiệu về SequenceReader của Steve Gordon giải thích cách lớp này có thể được sử dụng để đọc dữ liệu từ một luồng bằng cách sử dụng các dấu phân cách.

Thật không may, SequenceReaderlà một cấu trúc ref có nghĩa là nó không thể được sử dụng trong các phương thức không đồng bộ hoặc cục bộ. Đó là lý do Steve Gordon trong bài viết của mình tạo ra một

private static SequencePosition ReadItems(in ReadOnlySequence<byte> sequence, bool isCompleted)

phương thức để đọc các mục tạo thành một ReadOnlySequence và trả về vị trí kết thúc, do đó, TubeReader có thể tiếp tục từ đó. Thật không may, chúng tôi muốn trả về một phương thức IEnumerable hoặc IAsyncEnumerable và các phương thức lặp không thích inhoặc outtham số.

Chúng tôi có thể thu thập các mục được khử lưu lượng trong Danh sách hoặc Hàng đợi và trả lại chúng dưới dạng một kết quả duy nhất, nhưng điều đó vẫn sẽ phân bổ danh sách, bộ đệm hoặc nút và phải chờ tất cả các mục trong bộ đệm được khử lưu lượng trước khi quay lại:

private static (SequencePosition,List<T>) ReadItems(in ReadOnlySequence<byte> sequence, bool isCompleted)

Chúng ta cần một cái gì đó hoạt động như một vô số mà không yêu cầu một phương thức lặp, hoạt động với async và không đệm mọi thứ theo cách đó.

Thêm kênh để tạo IAsyncEnumerable

ChannelReader.ReadAllAsync trả về IAsyncEnumerable. Chúng tôi có thể trả về ChannelReader từ các phương thức không thể hoạt động như các trình vòng lặp và vẫn tạo ra một luồng các phần tử mà không lưu vào bộ đệm.

Điều chỉnh mã của Steve Gordon để sử dụng các kênh, chúng tôi có các phương thức ReadItems (ChannelWriter ...) ReadLastItem. Đầu tiên, đọc một mục tại một thời điểm, lên đến một dòng mới bằng cách sử dụng ReadOnlySpan<byte> itemBytes. Điều này có thể được sử dụng bởi JsonSerializer.Deserialize. Nếu ReadItemskhông thể tìm thấy dấu phân cách, nó sẽ trả về vị trí của nó để PipelineReader có thể kéo đoạn tiếp theo từ luồng.

Khi chúng ta đạt đến đoạn cuối cùng và không có dấu phân cách nào khác, ReadLastItem` đọc các byte còn lại và giải tuần tự hóa chúng.

Mã này gần giống với mã của Steve Gordon. Thay vì viết vào Bảng điều khiển, chúng tôi viết thư cho ChannelWriter.

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);
        }

    }    
}

Các DeserializeToChannel<T>phương pháp tạo ra một độc giả đường ống trên suối, tạo ra một kênh và bắt đầu một nhiệm vụ lao động mà phân tích khối và đẩy họ vào kênh:

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;
}

ChannelReader.ReceiveAllAsync()có thể được sử dụng để tiêu thụ tất cả các mặt hàng thông qua IAsyncEnumerable<T>:

var reader=DeserializeToChannel<MyEvent>(stream,cts.Token);
await foreach(var item in reader.ReadAllAsync(cts.Token))
{
    //Do something with it 
}    

0

Cảm giác như bạn cần phải đọc trình đọc luồng của riêng bạn. Bạn phải đọc từng byte một và dừng lại ngay khi hoàn thành định nghĩa đối tượng. Nó thực sự là cấp thấp. Như vậy, bạn sẽ KHÔNG tải toàn bộ tập tin vào RAM, mà chỉ lấy phần bạn đang xử lý. Nó dường như là một câu trả lời?


-2

Có lẽ bạn có thể sử dụng Newtonsoft.Jsonserializer? https://www.newtonsoft.com/json/help/html/Performance.htmlm

Đặc biệt xem phần:

Tối ưu hóa việc sử dụng bộ nhớ

Biên tập

Bạn có thể thử khử giá trị từ JsonTextReader, vd

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

Điều đó không trả lời câu hỏi. Đây hoàn toàn không phải về hiệu suất, đó là về truy cập phát trực tuyến mà không tải mọi thứ trong bộ nhớ
Panagiotis Kanavos

Bạn đã mở các liên kết liên quan hoặc chỉ nói những gì bạn nghĩ? Trong liên kết tôi đã gửi trong phần tôi đã đề cập, có một đoạn mã về cách khử tuần tự JSON từ luồng.
Miłosz Wieczorek

Vui lòng đọc lại câu hỏi - OP hỏi cách xử lý các yếu tố mà không khử lưu lượng mọi thứ trong bộ nhớ. Không chỉ đọc từ một luồng, mà chỉ xử lý những gì đến từ luồng. I don't want them to be in memory all at once, but I would rather read and process them one by one.Lớp có liên quan trong JSON.NET là JsonTextReader.
Panagiotis Kanavos

Trong mọi trường hợp, câu trả lời chỉ liên kết không được coi là câu trả lời hay và không có gì trong liên kết đó trả lời câu hỏi của OP. Liên kết đến JsonTextReader sẽ tốt hơn
Panagiotis Kanavos
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.