ลบ deserializing รายการโดยใช้ Asynchonously System.Text.Json


11

ให้บอกว่าฉันขอไฟล์ json ขนาดใหญ่ที่มีรายการวัตถุมากมาย ฉันไม่ต้องการให้พวกเขาอยู่ในความทรงจำทั้งหมดในครั้งเดียว แต่ฉันอยากอ่านและดำเนินการทีละคน ดังนั้นผมจึงจำเป็นต้องเปิด async กระแสเป็นSystem.IO.Stream IAsyncEnumerable<T>ฉันจะใช้System.Text.JsonAPI ใหม่เพื่อทำสิ่งนี้ได้อย่างไร

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
คุณอาจจะต้องสิ่งที่ต้องการDeserializeAsyncวิธี
พาเวล Anikhouski

2
ขออภัยดูเหมือนว่าวิธีการดังกล่าวจะโหลดสตรีมทั้งหมดลงในหน่วยความจำ คุณสามารถอ่านข้อมูลโดยใช้ asynchonously Utf8JsonReaderได้โปรดดูตัวอย่าง github และที่เธรดที่มีอยู่ด้วย
Pavel Anikhouski

GetAsyncด้วยตัวเองกลับมาเมื่อได้รับการตอบสนองทั้งหมด คุณต้องใช้SendAsyncกับ "HttpCompletionOption.ResponseContentRead" แทน เมื่อคุณมีที่คุณสามารถใช้ JSON.NET ของJsonTextReader ใช้System.Text.Jsonสำหรับการนี้ไม่ได้เป็นเรื่องง่ายที่เป็นปัญหานี้แสดงให้เห็นว่า ฟังก์ชันการทำงานไม่พร้อมใช้งานและนำไปใช้ในการจัดสรรที่ต่ำโดยใช้
struct

ปัญหาเกี่ยวกับการดีซีเรียลไลซ์เซอร์ในชิ้นส่วนก็คือคุณต้องรู้ว่าเมื่อใดที่คุณจะทำการดีซีเรียลไลซ์แบบสมบูรณ์ นี่จะเป็นการยากที่จะทำความสะอาดให้สำเร็จสำหรับกรณีทั่วไป มันจะต้องมีการแยกวิเคราะห์ล่วงหน้าซึ่งอาจเป็นการแลกเปลี่ยนที่ไม่ดีในแง่ของประสิทธิภาพ มันค่อนข้างยากที่จะพูดคุย แต่ถ้าคุณบังคับใช้ข้อ จำกัด ของตัวเองใน JSON ของคุณให้พูดว่า "วัตถุหนึ่งรายการตรงกับ 20 บรรทัดในไฟล์" จากนั้นคุณสามารถยกเลิกการซิงค์แบบอะซิงโครนัสได้โดยการอ่านไฟล์ใน chunks async คุณจะต้อง json ขนาดใหญ่เพื่อดูผลประโยชน์ที่นี่แม้ว่าฉันจะจินตนาการ
DetectivePikachu

ดูเหมือนว่ามีคนตอบคำถามที่คล้ายกันที่นี่พร้อมรหัสเต็ม
Panagiotis Kanavos

คำตอบ:


4

ใช่เครื่องมือเพิ่มประสิทธิภาพการสตรีม JSON (de) จะเป็นการปรับปรุงประสิทธิภาพที่ดีในหลาย ๆ สถานที่

น่าเสียดายที่System.Text.Jsonอย่าทำในเวลานี้ ฉันไม่แน่ใจว่ามันจะมีในอนาคต - ฉันหวังว่าอย่างนั้น! การสตรีมแบบ deserialization อย่างแท้จริงของ JSON กลายเป็นเรื่องที่ค่อนข้างท้าทาย

คุณสามารถตรวจสอบว่าUtf8Json ที่เร็วที่สุดรองรับได้หรือไม่

อย่างไรก็ตามอาจมีโซลูชันที่กำหนดเองสำหรับสถานการณ์เฉพาะของคุณเนื่องจากความต้องการของคุณดูเหมือนจะจำกัดความยากลำบาก

แนวคิดคือการอ่านทีละรายการจากอาเรย์ด้วยตนเอง เราใช้ประโยชน์จากความจริงที่ว่าแต่ละรายการในรายการนั้นเป็นวัตถุ JSON ที่ถูกต้อง

คุณสามารถข้ามผ่าน[(สำหรับรายการแรก) หรือ,(สำหรับแต่ละรายการถัดไป) ด้วยตนเอง แล้วฉันคิดว่าทางออกที่ดีที่สุดของคุณคือการใช้ .NET หลักของการกำหนดที่ปลายวัตถุปัจจุบันและอาหารสแกนไบต์Utf8JsonReaderJsonDeserializer

ด้วยวิธีนี้คุณจะทำการบัฟเฟอร์วัตถุครั้งละเล็กน้อยเท่านั้น

และเนื่องจากเรากำลังพูดถึงการแสดงคุณสามารถรับข้อมูลจาก PipeReaderในขณะที่คุณกำลังทำอยู่ :-)


นี่ไม่ได้เกี่ยวกับประสิทธิภาพเลย มันไม่ได้เกี่ยวกับการดีซีเรียลไลเซชั่นแบบasync ซึ่งมันทำอยู่แล้ว มันเกี่ยวกับการเข้าถึงการสตรีม - การประมวลผลองค์ประกอบ JSON เมื่อแยกวิเคราะห์จากสตรีมวิธีที่ JsonTextReader ของ JSON.NET ทำ
Panagiotis Kanavos

คลาสที่เกี่ยวข้องใน Utf8Json คือ JsonReader และอย่างที่ผู้เขียนบอกว่ามันแปลก JsonTextReader ของ JSON.NET และ System.Text.Json's Utf8JsonReader แบ่งปันความแปลกประหลาดแบบเดียวกัน - คุณต้องวนซ้ำและตรวจสอบประเภทขององค์ประกอบปัจจุบันตามที่คุณไป
Panagiotis Kanavos

@PanagiotisKanavos Ah ใช่สตรีมมิ่ง นั่นคือคำที่ฉันกำลังมองหา! ฉันกำลังอัพเดทคำว่า "asynchronous" เป็น "streaming" ฉันเชื่อว่าเหตุผลที่ต้องมีการสตรีมนั้น จำกัด การใช้หน่วยความจำซึ่งเป็นเรื่องเกี่ยวกับประสิทธิภาพ บางที OP สามารถยืนยันได้
Timo

ประสิทธิภาพไม่ได้หมายถึงความเร็ว ไม่ว่า deserializer จะรวดเร็วแค่ไหนหากคุณต้องประมวลผลรายการ 1M คุณไม่ต้องการเก็บไว้ใน RAM หรือรอให้ deserialized ทั้งหมดถูกประมวลผลก่อนที่คุณจะสามารถประมวลผลแรกได้
Panagiotis Kanavos

ความหมายเพื่อนของฉัน! ฉันดีใจที่เราพยายามทำสิ่งเดียวกันให้สำเร็จ
Timo

4

TL; DRมันไม่สำคัญเลย


ดูเหมือนว่ามีคนอยู่แล้ว โพสต์โค้ดเต็มรูปแบบสำหรับUtf8JsonStreamReaderstruct บัฟเฟอร์ที่อ่านจากกระแสและฟีดพวกเขาที่จะ Utf8JsonRreader ที่ช่วยให้ deserialization JsonSerializer.Deserialize<T>(ref newJsonReader, options);ง่ายด้วย รหัสไม่ได้เป็นเรื่องเล็กน้อย คำถามที่เกี่ยวข้องคือที่นี่และคำตอบคือที่นี่

แค่นั้นยังไม่เพียงพอ - HttpClient.GetAsyncจะกลับมาหลังจากได้รับการตอบสนองทั้งหมดโดยการบัฟเฟอร์ทุกอย่างในหน่วยความจำ

เพื่อหลีกเลี่ยงนี้HttpClient.GetAsync (สตริง HttpCompletionOption)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, AKA สตรีมมิ่ง JSON aka ... *

เป็นเรื่องปกติในการสตรีมเหตุการณ์หรือบันทึกสถานการณ์เพื่อต่อท้าย JSON แต่ละออบเจ็กต์กับไฟล์หนึ่งองค์ประกอบต่อบรรทัดเช่น:

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

นี่ไม่ใช่เอกสาร JSON ที่ถูกต้องแต่แต่ละแฟรกเมนต์นั้นถูกต้อง มีข้อดีหลายประการสำหรับข้อมูลขนาดใหญ่ / สถานการณ์พร้อมกันสูง การเพิ่มเหตุการณ์ใหม่ต้องใช้การต่อท้ายบรรทัดใหม่เข้ากับไฟล์เท่านั้นไม่ใช่การวิเคราะห์และสร้างไฟล์ใหม่ทั้งหมด การประมวลผลโดยเฉพาะการประมวลผลแบบขนานนั้นง่ายกว่าด้วยเหตุผลสองประการ:

  • แต่ละองค์ประกอบสามารถเรียกคืนได้ทีละครั้งเพียงแค่อ่านหนึ่งบรรทัดจากสตรีม
  • ไฟล์อินพุตสามารถแบ่งพาร์ติชันได้อย่างง่ายดายและแยกข้ามขอบเขตของบรรทัดป้อนแต่ละส่วนไปยังกระบวนการของผู้ปฏิบัติงานแยกต่างหากเช่นในคลัสเตอร์ Hadoop หรือเธรดที่แตกต่างกันในแอปพลิเคชัน: คำนวณคะแนนแยกเช่นโดยการหารความยาวด้วยจำนวนคนงาน จากนั้นมองหา newline แรก ให้อาหารทุกอย่างจนถึงจุดนั้นไปยังผู้ปฏิบัติงานแยกต่างหาก

ใช้ StreamReader

วิธีการจัดสรร -y ในการทำเช่นนี้คือการใช้ TextReader อ่านทีละบรรทัดแล้วแยกวิเคราะห์ด้วย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;
    }
}

นั่นง่ายกว่าโค้ดที่จะทำการหาอาร์เรย์ที่เหมาะสม มีสองประเด็น:

  • ReadLineAsync ไม่ยอมรับโทเค็นการยกเลิก
  • การวนซ้ำแต่ละครั้งจะจัดสรรสตริงใหม่ซึ่งเป็นหนึ่งในสิ่งที่เราต้องการหลีกเลี่ยงโดยใช้ System.Text.Json

นี่อาจจะเพียงพอแม้ว่าในขณะที่พยายามสร้างReadOnlySpan<Byte>บัฟเฟอร์ที่ JsonSerializer ต้องการ แต่การทดสอบไม่ได้เป็นเรื่องเล็กน้อย

ไปป์ไลน์และ SequenceReader

เพื่อหลีกเลี่ยงการจัดสรรเราจำเป็นต้องได้รับReadOnlySpan<byte>จากกระแส การทำเช่นนี้ต้องใช้ท่อ System.IO.Pipeline และSequenceReader struct คำแนะนำเบื้องต้นเกี่ยวกับลำดับผู้อ่านของ Steve Gordonอธิบายว่าคลาสนี้สามารถใช้อ่านข้อมูลจากสตรีมโดยใช้ตัวคั่นได้อย่างไร

แต่น่าเสียดายที่SequenceReaderเป็นโครงสร้างอ้างอิงซึ่งหมายความว่ามันไม่สามารถใช้ใน async หรือวิธีการในท้องถิ่น นั่นเป็นเหตุผลที่ Steve Gordon ในบทความของเขาสร้าง

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

วิธีการอ่านรายการในรูปแบบ ReadOnlySequence และส่งกลับตำแหน่งสิ้นสุดดังนั้น PipeReader สามารถดำเนินการต่อจากมัน น่าเสียดายที่เราต้องการคืนค่า IEnumerable หรือ IAsyncEnumerable และ iterator ไม่ชอบinหรือoutพารามิเตอร์

เราสามารถรวบรวมรายการที่ดีซีเรียลไลซ์ในรายการหรือคิวแล้วส่งคืนเป็นผลลัพธ์เดียว แต่ยังคงจัดสรรรายการบัฟเฟอร์หรือโหนดและต้องรอรายการทั้งหมดในบัฟเฟอร์ที่จะถูก deserialized ก่อนที่จะส่งคืน:

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

เราต้องการบางสิ่งที่ทำหน้าที่เหมือนตัวนับโดยไม่ต้องใช้วิธีตัววนซ้ำทำงานกับ async และไม่บัฟเฟอร์ทุกอย่าง

การเพิ่มแชเนลเพื่อสร้าง IAsyncEnumerable

ChannelReader.ReadAllAsyncส่งคืน IAsyncEnumerable เราสามารถส่งคืน ChannelReader จากวิธีที่ไม่สามารถใช้เป็นตัววนซ้ำและยังคงสร้างองค์ประกอบโดยไม่ต้องแคช

การปรับรหัสของสตีฟกอร์ดอนเพื่อใช้ช่องทางเราได้รับ ReadItems (ChannelWriter ... ) และReadLastItemวิธีการ ReadOnlySpan<byte> itemBytesคนแรกที่อ่านครั้งละหนึ่งรายการได้ถึงการขึ้นบรรทัดใหม่โดยใช้ JsonSerializer.Deserializeนี้สามารถนำมาใช้โดย ถ้าReadItemsไม่พบตัวคั่นมันจะส่งคืนตำแหน่งเพื่อให้ PipelineReader สามารถดึงชิ้นถัดไปจากสตรีม

เมื่อเราไปถึงก้อนสุดท้ายและไม่มีตัวคั่นอื่น ReadLastItem` จะอ่านจำนวนไบต์ที่เหลือและยกเลิกการเรียงลำดับ

รหัสเกือบจะเหมือนกับสตีฟกอร์ดอน แทนที่จะเขียนลงในคอนโซลเราเขียนถึง 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);
        }

    }    
}

DeserializeToChannel<T>วิธีการสร้างท่อส่งผู้อ่านที่ด้านบนของกระแสสร้างช่องทางและเริ่มเป็นงานที่คนงานที่จะแยกวิเคราะห์ชิ้นและผลักดันให้พวกเขาช่อง:

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()สามารถใช้เพื่อบริโภคสิ่งของทั้งหมดผ่านIAsyncEnumerable<T>:

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

0

รู้สึกเหมือนคุณต้องการปลูกฝังตัวอ่านสตรีมของคุณเอง คุณต้องอ่านไบต์หนึ่งต่อหนึ่งและหยุดทันทีที่การกำหนดวัตถุเสร็จสมบูรณ์ แน่นอนว่ามันค่อนข้างเลเวลต่ำ ดังนั้นคุณจะไม่โหลดไฟล์ทั้งหมดลงใน RAM แต่ควรมีส่วนร่วมกับคุณ ดูเหมือนว่าจะเป็นคำตอบหรือไม่?


-2

บางทีคุณอาจใช้Newtonsoft.Jsonserializer https://www.newtonsoft.com/json/help/html/Performance.htm

ดูหัวข้อโดยเฉพาะ:

ปรับการใช้หน่วยความจำให้เหมาะสม

แก้ไข

คุณสามารถลองทำการดีซีเรียลไลซ์ค่าจาก JsonTextReader เช่น

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

ไม่ตอบคำถาม นี่ไม่เกี่ยวกับประสิทธิภาพเลยมันเกี่ยวกับการเข้าถึงสตรีมโดยไม่โหลดทุกอย่างในหน่วยความจำ
Panagiotis Kanavos

คุณได้เปิดลิงก์ที่เกี่ยวข้องหรือแค่พูดในสิ่งที่คุณคิด ในลิงค์ที่ฉันส่งไปในส่วนที่ฉันพูดถึงมีข้อมูลโค้ดของวิธีการเลิก JSON จากสตรีม
Miłosz Wieczorek

โปรดอ่านคำถามอีกครั้ง - OP ถามถึงวิธีการประมวลผลองค์ประกอบโดยไม่ต้องทำการดีซีเรียลไลซ์ทุกอย่างในหน่วยความจำ ไม่เพียงอ่านจากสตรีมเท่านั้น แต่ประมวลผลสิ่งที่มาจากสตรีม I don't want them to be in memory all at once, but I would rather read and process them one by one.คลาสที่เกี่ยวข้องใน JSON.NET คือ JsonTextReader
Panagiotis Kanavos

ไม่ว่าในกรณีใดคำตอบเดียวที่เชื่อมโยงจะไม่ถือว่าเป็นคำตอบที่ดีและไม่มีสิ่งใดในลิงค์นั้นที่ตอบคำถามของ OP ลิงก์ไปยัง JsonTextReader น่าจะดีกว่า
Panagiotis Kanavos
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.