TL; DR Önemsiz değil
Birisi zaten bir akıştan arabellekleri okuyan ve bunları bir Utf8JsonRreader'a besleyen ve kolay serileştirmeye izin veren bir yapı için tam kod gönderdiğiniUtf8JsonStreamReader
gösteriyor JsonSerializer.Deserialize<T>(ref newJsonReader, options);
. Kod da önemsiz değil. İlgili soru burada ve cevap burada .
Bu yeterli değil - HttpClient.GetAsync
sadece tüm yanıt alındıktan sonra geri dönecek, aslında bellekteki her şeyi arabelleğe alacak.
Bunu önlemek için HttpClient.GetAsync (string, HttpCompletionOption) ile birlikte kullanılmalıdır.HttpCompletionOption.ResponseHeadersRead
.
Serileştirme döngüsü, iptal jetonunu da kontrol etmeli ve sinyal verilirse çıkmalı veya atmalıdır. Aksi takdirde, döngü tüm akış alınana ve işlenene kadar devam eder.
Bu kod ilgili cevabın örneğini temel alır ve kullanır HttpCompletionOption.ResponseHeadersRead
iptal jetonunu ve denetler. Uygun bir öğe dizisi içeren JSON dizelerini ayrıştırabilir, örneğin:
[{"prop1":123},{"prop1":234}]
İlk çağrı jsonStreamReader.Read()
dizinin başlangıcına, ikincisi ise ilk nesnenin başlangıcına gider. Dizinin sonu ( ]
) algılandığında döngünün kendisi sona erer .
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 parçaları, AKA akış JSON aka ... *
Olay akışında veya günlük kaydı senaryolarında bir dosyaya ayrı ayrı JSON nesneleri eklemek oldukça yaygındır.
{"eventId":1}
{"eventId":2}
...
{"eventId":1234567}
Bu geçerli bir JSON belgesi değil, tek tek parçalar geçerli. Bunun büyük veri / yüksek eşzamanlı senaryolar için çeşitli avantajları vardır. Yeni bir etkinlik eklemek, tüm dosyayı ayrıştırmak ve yeniden oluşturmak değil, yalnızca dosyaya yeni bir satır eklemenizi gerektirir. İşleme , özellikle paralel işleme iki nedenden dolayı daha kolaydır:
- Tek tek öğeler, bir akıştan bir satır okunarak tek tek alınabilir.
- Giriş dosyası kolayca bölünebilir ve çizgi sınırları boyunca bölünebilir, her parçayı ayrı bir çalışan işlemine, örneğin bir Hadoop kümesine veya bir uygulamadaki farklı iş parçacıklarına besleyebilir: Bölme noktalarını, örneğin uzunluğu çalışan sayısına bölerek hesaplayın. , sonra ilk yeni satırı arayın. Bu noktaya kadar her şeyi ayrı bir çalışanla besleyin.
StreamReader Kullanma
Bunu yapmak için tahsis-y yolu TextReader kullanmak olacaktır, her seferinde bir satır okumak ve onu ayrıştırmak 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;
}
}
Bu, uygun bir dizinin serisini kaldıran koddan çok daha basittir. İki sorun var:
ReadLineAsync
iptal jetonunu kabul etmiyor
- Her yineleme, System.Text.Json kullanarak kaçınmak istediğimiz şeylerden biri olan yeni bir dize ayırır.
ReadOnlySpan<Byte>
JsonSerializer'ın ihtiyaç duyduğu tamponları üretmeye çalışırken bu yeterli olabilir .
Boru Hatları ve Sıra Okuyucu
Tüm konumlardan kaçınmak için akıştan bir tane almamız gerekir ReadOnlySpan<byte>
. Bunun için System.IO.Pipeline boruları ve SequenceReader yapısının kullanılması gerekir. Steve Gordon , SequenceReader'a Giriş bu sınıfın sınırlayıcılar kullanarak bir akıştan veri okumak için nasıl kullanılabileceğini açıklar.
Ne yazık ki, SequenceReader
asenkron veya yerel yöntemlerde kullanılamayacağı anlamına gelen bir ref yapısıdır. Bu yüzden Steve Gordon makalesinde bir
private static SequencePosition ReadItems(in ReadOnlySequence<byte> sequence, bool isCompleted)
boru okuma yöntemi bir ReadOnlySequence oluşturur ve bitiş konumunu döndürür, böylece PipeReader bundan devam edebilir. Ne yazık ki bir IEnumerable veya IAsyncEnumerable döndürmek istiyoruz ve yineleyici yöntemleri de beğenmez in
veya out
parametreleri değiştirmez.
Diziselleştirilmiş öğeleri bir Liste veya Kuyrukta toplayıp tek bir sonuç olarak geri gönderebiliriz, ancak yine de listeler, arabellekler veya düğümler tahsis eder ve bir arabellekteki tüm öğelerin serileştirilmesini beklememiz gerekir:
private static (SequencePosition,List<T>) ReadItems(in ReadOnlySequence<byte> sequence, bool isCompleted)
Biz gerek bir şey , bir yineleyici yöntemi gerektirmeden bir enumerable gibi davranır zaman uyumsuz ile çalışır ve yolu her şeyi arabelleklenmez.
Bir IAsyncEnumerable üretmek için Kanal Ekleme
ChannelReader.ReadAllAsync bir IAsyncEnumerable döndürür. ChannelReader'ı yineleyici olarak çalışamayan yöntemlerden döndürebilir ve yine de önbelleğe almadan bir öğe akışı üretebiliriz.
Steve Gordon kodunu kanalları kullanacak şekilde uyarlayarak, ReadItems (ChannelWriter ...) ve ReadLastItem
yöntemlerini alıyoruz . Birincisi, kullanarak bir defada bir satır okur, yeni satır kullanarak ReadOnlySpan<byte> itemBytes
. Bu tarafından kullanılabilir JsonSerializer.Deserialize
. EğerReadItems
sınırlayıcı bulamıyorum PipelineReader akışından sonraki öbek çekin böylece, bu konumunu döndürür.
Son parçaya ulaştığımızda ve başka bir sınırlayıcı olmadığında, ReadLastItem `kalan baytları okur ve bunların serisini kaldırır.
Kod Steve Gordon'la neredeyse aynı. Konsola yazmak yerine ChannelWriter'a yazıyoruz.
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>
Yöntem, bir kanal yaratır akımının üstünde bir boru hattı okuyucu oluşturur ve bir alt görevi başlar kanala ayrıştırır parçalar ve iter onları:
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()
aşağıdakileri kullanarak tüm öğeleri tüketmek için kullanılabilir IAsyncEnumerable<T>
:
var reader=DeserializeToChannel<MyEvent>(stream,cts.Token);
await foreach(var item in reader.ReadAllAsync(cts.Token))
{
//Do something with it
}