TL; DRมันไม่สำคัญเลย
ดูเหมือนว่ามีคนอยู่แล้ว โพสต์โค้ดเต็มรูปแบบสำหรับUtf8JsonStreamReader
struct บัฟเฟอร์ที่อ่านจากกระแสและฟีดพวกเขาที่จะ 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
}