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 विधि
Pavel Anikhouski

2
क्षमा करें, ऐसा लगता है कि ऊपर की विधि मेमोरी में संपूर्ण स्ट्रीम को लोड करती है। आप मात्रा asynchonously का उपयोग करके डेटा पढ़ सकते हैं Utf8JsonReader, कृपया कुछ GitHub पर एक नजर है नमूने और मौजूदा पर धागा रूप में अच्छी तरह
पावेल Anikhouski

GetAsyncजब पूरी प्रतिक्रिया प्राप्त हो जाती है, तब ही वापस लौटता है। आपको SendAsyncइसके बजाय `HttpCompletionOption.ResponseContentRead` के साथ उपयोग करने की आवश्यकता है । एक बार जब आपके पास है कि आप JSON.NET के JsonTextReader का उपयोग कर सकते हैं । इसके लिए उपयोग System.Text.Jsonकरना उतना आसान नहीं है जितना कि यह मुद्दा दिखाता है । कार्यक्षमता उपलब्ध नहीं है और इसे कम-आवंटन में लागू करने से
स्ट्रक्चर्स

विखंडू में deserializing के साथ समस्या यह है कि आपको पता है कि जब आप deserialize करने के लिए एक पूर्ण हिस्सा है। यह सामान्य मामलों के लिए सफाई से पूरा करना मुश्किल होगा। इसे पहले से पार्स करने की आवश्यकता होगी, जो प्रदर्शन के मामले में काफी खराब व्यापार हो सकता है। इसे सामान्य करना मुश्किल होगा। लेकिन अगर आप अपने JSON पर अपने स्वयं के प्रतिबंधों को लागू करते हैं, तो कहें "एक एकल ऑब्जेक्ट फ़ाइल में बिल्कुल 20 पंक्तियों पर कब्जा कर लेता है", तो आप अनिवार्य रूप से फाइल को चोंच async में पढ़कर अतुल्यकालिक रूप से deserialize कर सकते हैं। हालांकि, मुझे यहाँ लाभ देखने के लिए बड़े पैमाने पर जॅसन की आवश्यकता होगी।
डिटेक्टिवपिकाचू

ऐसा लगता है कि किसी ने पहले ही पूर्ण कोड के साथ यहां एक समान प्रश्न का उत्तर दिया है
पनगीओटीस कानावोस

जवाबों:


4

हां, वास्तव में स्ट्रीमिंग JSON (डी) के धारावाहिक में इतने सारे स्थानों पर एक अच्छा प्रदर्शन सुधार होगा।

दुर्भाग्य से, System.Text.Jsonइस समय ऐसा नहीं करता है। मुझे यकीन नहीं है कि यह भविष्य में होगा - मुझे उम्मीद है! वास्तव में JSON का स्ट्रीमिंग स्ट्रीमिंग डीरियलाइज़ेशन बल्कि चुनौतीपूर्ण हो जाता है।

आप जाँच कर सकते हैं कि अत्यंत तेज़ Utf8Json इसका समर्थन करता है या नहीं।

हालाँकि, आपकी विशिष्ट स्थिति के लिए एक कस्टम समाधान हो सकता है, क्योंकि आपकी आवश्यकताएं कठिनाई का कारण बनती हैं।

यह विचार एक समय में सरणी से एक आइटम को मैन्युअल रूप से पढ़ने के लिए है। हम इस तथ्य का उपयोग कर रहे हैं कि सूची में प्रत्येक आइटम, अपने आप में, एक वैध JSON ऑब्जेक्ट है।

आप मैन्युअल रूप से [(पहले आइटम के लिए) या ,(प्रत्येक अगले आइटम के लिए) को छोड़ सकते हैं। तब मुझे लगता है कि आपकी सबसे अच्छी शर्त यह है Utf8JsonReaderकि वर्तमान वस्तु समाप्त होने का निर्धारण करने के लिए .NET कोर का उपयोग करें , और स्कैन किए गए बाइट्स को खिलाएं JsonDeserializer

इस तरह, आप केवल एक समय में एक वस्तु पर थोड़ा बफर कर रहे हैं।

और जब से हम प्रदर्शन के बारे में बात कर रहे हैं, आप इनपुट प्राप्त कर सकते हैं PipeReader, जबकि आप इस पर हैं। :-)


यह प्रदर्शन के बारे में बिल्कुल नहीं है। यह async deserialization के बारे में नहीं है, जो यह पहले से ही करता है। यह स्ट्रीमिंग एक्सेस के बारे में है - JSON तत्वों को संसाधित करना, क्योंकि वे स्ट्रीम से पार्स किए जाते हैं, जिस तरह से JSON.NET का JsonTextRadder करता है।
पनियागोटिस कानावोस

Utf8Json में संबंधित वर्ग JsonReader है और जैसा कि लेखक कहता है, यह अजीब है। JSON.NET का JsonTextReader और System.Text.Json का Utf8JsonReader समान विचित्रता साझा करता है - आपको चलते हुए वर्तमान तत्व के प्रकार को लूप और चेक करना होगा।
पैनागोटिस कानावोस

@PanagiotisKanavos आह, हाँ, स्ट्रीमिंग। यही वह शब्द है जिसकी मुझे तलाश थी! मैं "एसिंक्रोनस" शब्द को "स्ट्रीमिंग" में अपडेट कर रहा हूं। मेरा मानना ​​है कि स्ट्रीमिंग करने का कारण स्मृति उपयोग को सीमित करना है, जो एक प्रदर्शन चिंता का विषय है। शायद ओपी पुष्टि कर सकता है।
टिमो

प्रदर्शन का मतलब गति नहीं है। डिसेरिएलाइज़र कितना भी तेज़ क्यों न हो, अगर आपको 1M आइटम को प्रोसेस करना है, तो आप उन्हें रैम में स्टोर नहीं करना चाहते हैं, और न ही पहले वाले को प्रोसेस करने से पहले उन सभी को इंतज़ार कर सकते हैं।
पनियागोटिस कानावोस

शब्दार्थ, मेरे मित्र! मुझे खुशी है कि हम सब के बाद एक ही चीज हासिल करने की कोशिश कर रहे हैं।
तिमो

4

टीएल; डीआर यह तुच्छ नहीं है


ऐसा लगता है कि किसी ने पहले से ही एक संरचना के लिए पूर्ण कोड पोस्ट किया हैUtf8JsonStreamReader जो एक स्ट्रीम से बफ़र पढ़ता है और उन्हें 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 उर्फ ​​... *

ईवेंट स्ट्रीमिंग या लॉगिंग परिदृश्यों में व्यक्तिगत JSON ऑब्जेक्ट्स को एक फ़ाइल, प्रति पंक्ति एक तत्व जैसे:

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

यह एक वैध JSON दस्तावेज़ नहीं है, लेकिन अलग-अलग टुकड़े मान्य हैं। बड़े डेटा / अत्यधिक समवर्ती परिदृश्यों के लिए इसके कई फायदे हैं। एक नई घटना को जोड़ने के लिए केवल फ़ाइल के लिए एक नई पंक्ति को जोड़ना आवश्यक है, संपूर्ण फ़ाइल को पार्स करना और पुनर्निर्माण नहीं करना। प्रसंस्करण , विशेष रूप से समानांतर प्रसंस्करण दो कारणों से आसान है:

  • अलग-अलग तत्वों को एक बार में एक स्ट्रीम से एक लाइन को पढ़कर, एक बार में ही प्राप्त किया जा सकता है।
  • इनपुट फ़ाइल को आसानी से विभाजित किया जा सकता है और लाइन की सीमाओं के पार विभाजित किया जा सकता है, प्रत्येक भाग को एक अलग कार्यकर्ता प्रक्रिया में खिलाया जा सकता है, जैसे कि एक हडॉप क्लस्टर में, या बस एक आवेदन में अलग-अलग थ्रेड्स: विभाजित अंकों की गणना करें जैसे कि श्रमिकों की संख्या से लंबाई को विभाजित करके। , फिर पहले नईलाइन देखें। एक अलग कार्यकर्ता को उस बिंदु तक सब कुछ खिलाएं।

एक StreamReader का उपयोग करना

यह करने के लिए आवंटित-य रास्ता एक 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.Deserialize द्वारा आवश्यक बफ़र्स का उत्पादन करने का प्रयास तुच्छ नहीं है।

पाइपलाइन और अनुक्रमर

आवंटन से बचने के लिए, हमें ReadOnlySpan<byte>धारा से प्राप्त करने की आवश्यकता है । ऐसा करने के लिए System.IO.Pipline पाइप और SequenceReader संरचना का उपयोग करना पड़ता है। स्टीव गॉर्डन का सीक्वेंसरीडर का एक परिचय बताता है कि कैसे इस वर्ग का उपयोग सीमांकक का उपयोग करके एक धारा से डेटा पढ़ने के लिए किया जा सकता है।

दुर्भाग्य से, SequenceReaderएक रेफ स्ट्रक्चर है जिसका अर्थ है कि इसका उपयोग एसिंक्स या स्थानीय तरीकों में नहीं किया जा सकता है। यही कारण है कि स्टीव गॉर्डन ने अपने लेख में ए

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

आइटम पढ़ने के लिए विधि ReadOnlySequence बनाते हैं और समाप्त होने वाली स्थिति को वापस करते हैं, इसलिए पाइप-राइडर इससे फिर से शुरू कर सकता है। दुर्भाग्य से हम एक IEnumerable या IAsyncEnumerable लौटना चाहते हैं, और पुनरावृत्त तरीके inया outपैरामीटर पसंद नहीं करते हैं।

हम एक सूची या कतार में deserialized आइटम एकत्र कर सकते हैं और उन्हें एक ही परिणाम के रूप में वापस कर सकते हैं, लेकिन वह अभी भी सूची, बफ़र या नोड्स आवंटित करेगा और लौटने से पहले deserialized होने के लिए बफर में सभी वस्तुओं की प्रतीक्षा करनी होगी:

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

हमें कुछ ऐसा चाहिए जो एक इटेरेटर विधि की आवश्यकता के बिना एक एन्यूमरेबल की तरह काम करता है, जो कि एसिंक्स के साथ काम करता है और हर तरह से बफर नहीं करता है।

एक IAsyncEnumerable का उत्पादन करने के लिए चैनल जोड़ना

ChannelReader.ReadAllAsync एक IAsyncEnumerable देता है। हम उन तरीकों से एक ChannelReader लौटा सकते हैं जो पुनरावृत्तियों के रूप में काम नहीं कर सकते हैं और अभी भी कैशिंग के बिना तत्वों की एक धारा का उत्पादन कर सकते हैं।

चैनलों का उपयोग करने के लिए स्टीव गॉर्डन के कोड को अपनाने से हमें ReadItems (ChannelWriter ...) और ReadLastItemतरीके मिलते हैं । पहले वाला, एक बार में एक आइटम पढ़ता है, एक नई लाइन का उपयोग करके ReadOnlySpan<byte> itemBytes। इसके द्वारा इस्तेमाल किया जा सकता है JsonSerializer.Deserialize। यदि ReadItemsसीमांकक नहीं मिल रहा है, तो यह अपनी स्थिति लौटाता है, इसलिए पाइपलाइनलाइनर अगले चंक को धारा से खींच सकता है।

जब हम अंतिम भाग तक पहुँचते हैं और कोई अन्य सीमांकक नहीं होता है, तो 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.Jsonधारावाहिक का उपयोग कर सकें ? 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;
    }
}

इस सवाल का जवाब नहीं है। यह प्रदर्शन के बारे में बिल्कुल नहीं है, यह मेमोरी में सब कुछ लोड किए बिना स्ट्रीमिंग एक्सेस के बारे में है
पैनागोटिस कानवास

क्या आपने संबंधित लिंक खोला है या सिर्फ वही कहा है जो आप सोचते हैं? मैंने जिस खंड का उल्लेख किया है, उस लिंक में मैंने भेजा है कि कैसे JSON को धारा से अलग करना एक कोड स्निपेट है।
15-25

कृपया प्रश्न को फिर से पढ़ें - ओपी पूछता है कि स्मृति में सब कुछ विहीन किए बिना तत्वों को कैसे संसाधित किया जाए । न केवल एक स्ट्रीम से पढ़ें, बल्कि केवल स्ट्रीम से जो आता है उसे प्रोसेस करें। 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 है।
पैनागोटिस कानावोस

किसी भी मामले में, लिंक-ओनली उत्तर को एक अच्छा उत्तर नहीं माना जाता है, और उस लिंक में कुछ भी ओपी के प्रश्न का उत्तर नहीं देता है। JsonTextReader के लिए एक लिंक बेहतर होगा
Panagiotis Kanavos
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.