Есть ли способ получать данные последовательно без ожидания загрузки всех элементов сразу?
Что проверяют
Знаете ли вы IAsyncEnumerable<T> и видите ли разницу между «асинхронно получить весь список» и «асинхронно получать элементы по одному». Следом часто спрашивают про EF Core, ASP.NET Core и отмену перебора.
Как отвечать
Есть два механизма, и важно их не путать:
IEnumerable<T>сyield return: ленивый синхронный итератор. Элемент вычисляется, когда потребитель его запросил, но если внутри есть ожидание сети или диска, поток блокируется;IAsyncEnumerable<T>(с C# 8 и .NET Core 3.0): асинхронный итератор. Метод объявляется какasync IAsyncEnumerable<T>, внутри допустимы иawait, иyield return, а потребитель читает черезawait foreach. Каждый элемент обрабатывается сразу после получения, память не растёт с размером выборки.
Где это применяется на практике:
- EF Core:
AsAsyncEnumerable()читает строки по мере прихода из открытогоDataReader, тогда какToListAsync()загружает всё в память; - ASP.NET Core: действие контроллера может вернуть
IAsyncEnumerable<T>, иSystem.Text.Jsonотдаст JSON-массив потоком, не собирая его целиком; - клиент:
JsonSerializer.DeserializeAsyncEnumerable<T>()читает элементы массива из потока ответа, не дожидаясь его конца; - производитель и потребитель в разных задачах:
System.Threading.Channels, если нужна очередь с ограничением размера.
Отмену передают через CancellationToken с атрибутом [EnumeratorCancellation] на параметре итератора, а снаружи вызывают .WithCancellation(token). Операторы LINQ для асинхронных последовательностей долго жили в пакете System.Linq.Async, в свежих версиях .NET они вошли в стандартную библиотеку.
Где спотыкаются
Предлагают async Task<List<T>> как ответ на вопрос. Это асинхронно, но результат приходит только целиком, ровно то, чего просили избежать.
Держат итератор EF Core открытым долго и забывают, что за ним стоят открытое соединение и DbContext, который нельзя параллельно использовать для других запросов.
Забывают про отмену: без [EnumeratorCancellation] токен из WithCancellation() до итератора не доходит, и перебор продолжается после того, как клиент отключился.