टीएल; डीआर यह तुच्छ नहीं है
ऐसा लगता है कि किसी ने पहले से ही एक संरचना के लिए पूर्ण कोड पोस्ट किया है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
}