System.Text.Json kullanarak bir listenin zaman uyumsuz olarak serisini kaldırma


11

Pek çok nesnenin listesini içeren büyük bir json dosyası istediğimi varsayalım. Bir kerede hafızada olmalarını istemiyorum, ama onları tek tek okumak ve işlemek istiyorum. Bu yüzden bir zaman uyumsuz System.IO.Streamakışı bir IAsyncEnumerable<T>. Bunu yapmak için yeni System.Text.JsonAPI'yı nasıl kullanabilirim ?

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
Muhtemelen DeserializeAsync yöntemi gibi bir şeye ihtiyacınız olacak
Pavel Anikhouski

2
Üzgünüz, yukarıdaki yöntem tüm akışı belleğe yüklüyor. Verileri asynchonously kullanarak parçaları okuyabilirsiniz Utf8JsonReader, lütfen bazı github örneklerine ve mevcut iş parçacığına da bir göz atın
Pavel Anikhouski

GetAsynczaman kendiliğinden geri bütün yanıt alınır. Bunun SendAsyncyerine `HttpCompletionOption.ResponseContentRead 'ile kullanmanız gerekir . Bir kez sen JSON.NET JsonTextReader kullanabilirsiniz . Bunun System.Text.Jsoniçin kullanımı bu sorunun gösterdiği kadar kolay değil . İşlevler mevcut değildir ve yapıları kullanarak düşük bir ayırmada uygulamak önemsiz değildir
Panagiotis Kanavos

Parçalarda serileştirme ile ilgili sorun, serileştirmeyi kaldırmak için tam bir parçaya sahip olmanız gerektiğini bilmenizdir. Genel vakalar için bunu başarmak zor olurdu. Önceden ayrışmayı gerektirecekti, bu da performans açısından oldukça zayıf bir takas olabilir. Genellemek oldukça zor olurdu. Ancak, JSON'unuzda kendi kısıtlamalarınızı uygularsanız, "tek bir nesne dosyada tam olarak 20 satır kaplar" deyin, o zaman dosyaları asenkron olarak parçalar zaman uyumsuz olarak okuyarak zaman uyumsuz olarak serileştirmeyi kaldırabilirsiniz. Yine de burada fayda görmek için büyük bir json'a ihtiyacınız olacak, sanırım.
DetectivePikachu

Biri gibi görünüyor zaten cevap benzer soru burada tam koduyla.
Panagiotis Kanavos

Yanıtlar:


4

Evet, gerçekten akışlı bir JSON (de) serileştiricisi pek çok yerde olması iyi bir performans iyileştirmesi olacaktır.

Ne yazık ki, System.Text.Jsonşu anda bunu yapmıyor. Gelecekte olup olmayacağından emin değilim - umarım! JSON'un gerçek anlamda akışının serileştirilmesi oldukça zorlayıcı.

Belki de son derece hızlı Utf8Json'un destekleyip desteklemediğini kontrol edebilirsiniz .

Ancak, gereksinimleriniz zorluğu kısıtladığından, özel durumunuz için özel bir çözüm olabilir.

Fikir diziden bir kerede bir öğeyi manuel olarak okumaktır. Listedeki her öğenin kendi başına geçerli bir JSON nesnesi olması gerçeğinden yararlanıyoruz.

Manuel olarak [(ilk öğe için) veya ,(sonraki her öğe için ) geçebilirsiniz . Sonra en iyi bahis Utf8JsonReadergeçerli nesnenin nerede biteceğini belirlemek için .NET Core kullanmak ve taranan bayt beslemek olduğunu düşünüyorum JsonDeserializer.

Bu şekilde, bir kerede yalnızca bir nesnenin üzerinde biraz arabelleğe alınıyorsunuz.

Performanstan bahsettiğimiz için, siz adayken a'dan girdiyi alabilirsiniz PipeReader. :-)


Bu hiç performansla ilgili değil. O zaman uyumsuz seri kaldırma, ilgili değil zaten yok. Akış erişimi ile ilgilidir - JSON öğelerini, JSON.NET'in JsonTextReader'ın yaptığı gibi akıştan ayrıştırılırken işlemekle ilgilidir.
Panagiotis Kanavos

Utf8Json'daki ilgili sınıf JsonReader ve yazarın söylediği gibi garip. JSON.NET'in JsonTextReader ve System.Text.Json'un Utf8JsonReader'ı aynı tuhaflığı paylaşıyor - gittikçe geçerli öğenin türünü döngüye almanız ve kontrol etmeniz gerekiyor.
Panagiotis Kanavos

@PanagiotisKanavos Ah, evet, akış. Aradığım kelime bu! "Eşzamansız" kelimesini "akış" olarak güncelliyoruz. Akış istemenin sebebinin bellek kullanımını sınırlamak olduğuna inanıyorum ki bu bir performans kaygısıdır. Belki OP onaylayabilir.
Timo

Performans hız demek değildir. Eğer 1M öğeleri işlemek zorunda olursa olsun deserializer ne kadar hızlı, sen yok RAM bunları saklamak istiyor, ne de ilkini işleyebilir önce hepsi için beklemek serileştirilemezse almak.
Panagiotis Kanavos

Anlambilim, dostum! Ne de olsa aynı şeyi başarmaya çalıştığımıza sevindim.
Timo

4

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, SequenceReaderasenkron 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 inveya outparametreleri 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 ReadLastItemyö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 
}    

0

Kendi akış okuyucunuzu implant etmeniz gerekiyor gibi görünüyor. Baytları tek tek okumalı ve nesne tanımı tamamlanır tamamlanmaz durmalısınız. Gerçekten oldukça düşük seviyelidir. Bu nedenle, dosyanın tamamını RAM'e YÜKLEMEYECEK, daha çok uğraştığınız kısmı almazsınız. Bir cevap gibi görünüyor mu?


-2

Belki Newtonsoft.Jsonserileştiriciyi kullanabilirsin ? https://www.newtonsoft.com/json/help/html/Performance.htm

Özellikle bölüme bakınız:

Bellek Kullanımını Optimize Edin

Düzenle

JsonTextReader'daki değerlerin serisini kaldırmayı deneyebilirsiniz, örn.

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

Bu soruya cevap vermiyor. Bu akış erişimi ilgili, hiç performans ilgili değil olmadan bellekte yükleme herşeyi
Panagiotis Kanavos

İlgili bağlantıyı açtınız mı veya sadece ne düşündüğünüzü söylediniz mi? Bahsettiğim bölümde gönderdiğim linkte, JSON'un akıştan serisini nasıl kaldıracağına dair bir kod snippet'i var.
Miłosz Wieczorek

Soruyu tekrar okuyun lütfen - OP elemanlarını nasıl işleneceğine sorar olmadan bellekte şeyi deserializing. Sadece bir akıştan okumakla kalmaz, yalnızca akıştan gelenleri işler. 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 içinde ilgili sınıf JsonTextReader olduğunu.
Panagiotis Kanavos

Her durumda, yalnızca bağlantı yanıtı iyi bir yanıt olarak kabul edilmez ve bu bağlantıdaki hiçbir şey OP'nin sorusunu yanıtlamaz. JsonTextReader bağlantısı daha iyi olurdu
Panagiotis Kanavos
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.