TL; DR Nó không tầm thường
Có vẻ như ai đó đã đăng mã đầy đủ cho một Utf8JsonStreamReader
cấu trúc đọc bộ đệm từ luồng và đưa chúng vào Utf8JsonRreader, cho phép dễ dàng giải nén JsonSerializer.Deserialize<T>(ref newJsonReader, options);
. Mã này cũng không tầm thường. Câu hỏi liên quan là ở đây và câu trả lời là ở đây .
Điều đó vẫn chưa đủ - HttpClient.GetAsync
sẽ chỉ trở lại sau khi nhận được toàn bộ phản hồi, về cơ bản là đệm mọi thứ trong bộ nhớ.
Để tránh điều này, nên sử dụng httpClient.GetAsync (chuỗi, httpCompletionOption)HttpCompletionOption.ResponseHeadersRead
.
Vòng lặp khử lưu huỳnh cũng nên kiểm tra mã thông báo hủy và thoát hoặc ném nếu nó được báo hiệu. Nếu không, vòng lặp sẽ tiếp tục cho đến khi toàn bộ luồng được nhận và xử lý.
Mã này dựa trên ví dụ của câu trả lời liên quan và sử dụng HttpCompletionOption.ResponseHeadersRead
và kiểm tra mã thông báo hủy. Nó có thể phân tích các chuỗi JSON có chứa một mảng các mục thích hợp, ví dụ:
[{"prop1":123},{"prop1":234}]
Cuộc gọi đầu tiên jsonStreamReader.Read()
di chuyển đến điểm bắt đầu của mảng trong khi cuộc gọi thứ hai di chuyển đến điểm bắt đầu của đối tượng đầu tiên. Vòng lặp tự chấm dứt khi kết thúc mảng ( ]
) được phát hiện.
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();
}
}
Các đoạn JSON, AKA phát trực tuyến JSON aka ... *
Nó khá phổ biến trong các tình huống phát trực tuyến hoặc ghi nhật ký để nối các đối tượng JSON riêng lẻ vào một tệp, một phần tử trên mỗi dòng, ví dụ:
{"eventId":1}
{"eventId":2}
...
{"eventId":1234567}
Đây không phải là một tài liệu JSON hợp lệ nhưng các đoạn riêng lẻ là hợp lệ. Điều này có một số lợi thế cho dữ liệu lớn / kịch bản đồng thời cao. Thêm một sự kiện mới chỉ yêu cầu nối thêm một dòng mới vào tệp, không phân tích và xây dựng lại toàn bộ tệp. Xử lý , đặc biệt là xử lý song song dễ dàng hơn vì hai lý do:
- Các yếu tố riêng lẻ có thể được truy xuất từng cái một, chỉ bằng cách đọc một dòng từ một luồng.
- Tệp đầu vào có thể dễ dàng phân vùng và phân chia theo ranh giới dòng, cung cấp từng phần cho một quy trình công nhân riêng biệt, ví dụ như trong cụm Hadoop hoặc đơn giản là các luồng khác nhau trong một ứng dụng: Tính các điểm phân chia, ví dụ bằng cách chia độ dài cho số lượng công nhân , sau đó tìm kiếm dòng mới đầu tiên. Cho ăn tất cả mọi thứ cho đến thời điểm đó cho một công nhân riêng biệt.
Sử dụng StreamReader
Cách phân bổ để làm điều này sẽ là sử dụng TextReader, đọc từng dòng một và phân tích cú pháp với 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;
}
}
Điều đó đơn giản hơn nhiều so với mã giải mã một mảng thích hợp. Có hai vấn đề:
ReadLineAsync
không chấp nhận mã thông báo hủy
- Mỗi lần lặp phân bổ một chuỗi mới, một trong những điều chúng tôi muốn tránh bằng cách sử dụng System.Text.Json
Điều này có thể là đủ mặc dù khi cố gắng tạo ra các ReadOnlySpan<Byte>
bộ đệm cần thiết bởi JsonSerializer.Deserialize không tầm thường.
Đường ống và trình tự
Để tránh sự phân bổ, chúng ta cần lấy một ReadOnlySpan<byte>
luồng từ luồng. Làm điều này đòi hỏi phải sử dụng các ống System.IO.Pipeline và cấu trúc SequenceReader . Giới thiệu về SequenceReader của Steve Gordon giải thích cách lớp này có thể được sử dụng để đọc dữ liệu từ một luồng bằng cách sử dụng các dấu phân cách.
Thật không may, SequenceReader
là một cấu trúc ref có nghĩa là nó không thể được sử dụng trong các phương thức không đồng bộ hoặc cục bộ. Đó là lý do Steve Gordon trong bài viết của mình tạo ra một
private static SequencePosition ReadItems(in ReadOnlySequence<byte> sequence, bool isCompleted)
phương thức để đọc các mục tạo thành một ReadOnlySequence và trả về vị trí kết thúc, do đó, TubeReader có thể tiếp tục từ đó. Thật không may, chúng tôi muốn trả về một phương thức IEnumerable hoặc IAsyncEnumerable và các phương thức lặp không thích in
hoặc out
tham số.
Chúng tôi có thể thu thập các mục được khử lưu lượng trong Danh sách hoặc Hàng đợi và trả lại chúng dưới dạng một kết quả duy nhất, nhưng điều đó vẫn sẽ phân bổ danh sách, bộ đệm hoặc nút và phải chờ tất cả các mục trong bộ đệm được khử lưu lượng trước khi quay lại:
private static (SequencePosition,List<T>) ReadItems(in ReadOnlySequence<byte> sequence, bool isCompleted)
Chúng ta cần một cái gì đó hoạt động như một vô số mà không yêu cầu một phương thức lặp, hoạt động với async và không đệm mọi thứ theo cách đó.
Thêm kênh để tạo IAsyncEnumerable
ChannelReader.ReadAllAsync trả về IAsyncEnumerable. Chúng tôi có thể trả về ChannelReader từ các phương thức không thể hoạt động như các trình vòng lặp và vẫn tạo ra một luồng các phần tử mà không lưu vào bộ đệm.
Điều chỉnh mã của Steve Gordon để sử dụng các kênh, chúng tôi có các phương thức ReadItems (ChannelWriter ...) ReadLastItem
. Đầu tiên, đọc một mục tại một thời điểm, lên đến một dòng mới bằng cách sử dụng ReadOnlySpan<byte> itemBytes
. Điều này có thể được sử dụng bởi JsonSerializer.Deserialize
. Nếu ReadItems
không thể tìm thấy dấu phân cách, nó sẽ trả về vị trí của nó để PipelineReader có thể kéo đoạn tiếp theo từ luồng.
Khi chúng ta đạt đến đoạn cuối cùng và không có dấu phân cách nào khác, ReadLastItem` đọc các byte còn lại và giải tuần tự hóa chúng.
Mã này gần giống với mã của Steve Gordon. Thay vì viết vào Bảng điều khiển, chúng tôi viết thư cho 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);
}
}
}
Các DeserializeToChannel<T>
phương pháp tạo ra một độc giả đường ống trên suối, tạo ra một kênh và bắt đầu một nhiệm vụ lao động mà phân tích khối và đẩy họ vào kênh:
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()
có thể được sử dụng để tiêu thụ tất cả các mặt hàng thông qua IAsyncEnumerable<T>
:
var reader=DeserializeToChannel<MyEvent>(stream,cts.Token);
await foreach(var item in reader.ReadAllAsync(cts.Token))
{
//Do something with it
}