Підрозділ 17.6
Асинхронні потоки. IAsyncEnumerable<T>
17.6. Асинхронні потоки. IAsyncEnumerable<T Класичний async Task<T відповідає на одне питання: «дай мені результат асинхронно». Але що, якщо результатів багато і вони надходять поступово — рядки з великого файл
17.6. Асинхронні потоки. IAsyncEnumerable
Класичний async Task<T> відповідає на одне питання: «дай мені результат асинхронно». Але що, якщо результатів багато і вони надходять поступово — рядки з великого файлу, повідомлення з черги, дані з БД мільйонами рядків? Завантажити все у пам'ять і повернути Task<List<T>> — неефективно або неможливо. Саме для цього C# 8.0 ввів IAsyncEnumerable<T> — асинхронні потоки даних, де кожен елемент може бути отриманий асинхронно.

Проблема: завантажити все vs обробляти по мірі надходження
Порівняємо два підходи для завантаження великого набору медичних записів:
При IAsyncEnumerable<T> перший елемент доступний через ~50 мс, а не через 250 мс. Користувач бачить результати миттєво — це принципова різниця у сприйнятті швидкості.
Оголошення та yield return
Метод, що повертає IAsyncEnumerable<T>, використовує:
asyncу заголовкуIAsyncEnumerable<T>як тип поверненняyield returnдля кожного елементаawaitміж елементами для асинхронних операцій
yield return у async-методі — це точка, де метод «призупиняється» і повертає елемент споживачу. Споживач обробляє елемент, і лише потім метод продовжує генерувати наступний.
await foreach — споживання асинхронного потоку
await foreach — спеціальна синтаксична конструкція для обходу IAsyncEnumerable<T>. Вона асинхронно очікує кожен наступний елемент і виконує тіло циклу:
Компілятор перетворює await foreach на виклики GetAsyncEnumerator(), MoveNextAsync() та Current — async-аналоги синхронного IEnumerable<T>.
CancellationToken у асинхронних потоках
IAsyncEnumerable<T> підтримує скасування через атрибут [EnumeratorCancellation] у параметрах методу-генератора. Це дозволяє передавати токен через WithCancellation() при await foreach:
Без [EnumeratorCancellation] токен, переданий через WithCancellation(), не потрапить у параметри методу-генератора і не спрацює.
Фільтрація та трансформація потоку
Асинхронні потоки можна обробляти у pipeline: один метод генерує, інший фільтрує або трансформує. Це async-аналог LINQ-ланцюгів для синхронних колекцій:
Кожен метод у pipeline є незалежним async-генератором, що не зберігає всі дані у пам'яті — передає елемент далі одразу при отриманні.
IAsyncEnumerable vs інші підходи
| Підхід | Повертає | Споживання | Зберігає в пам'яті | Підходить для |
|---|---|---|---|---|
Task<List<T>> |
Весь список одразу | await + foreach |
Весь список | Малі набори даних |
IEnumerable<T> |
Елементи по одному | foreach |
Один елемент | Синхронна генерація |
IAsyncEnumerable<T> |
Елементи по одному | await foreach |
Один елемент | Async-генерація, стрімінг |
Channel<T> |
Черга повідомлень | ReadAllAsync() |
Буфер | Продюсер-споживач |
IAsyncEnumerable<T> — правильний вибір, коли:
- Джерело даних велике або нескінченне (БД-курсор, черга, сенсорний потік)
- Перший результат потрібен якнайшвидше
- Продюсер і споживач можуть мати різну швидкість
- Обробка кожного елемента асинхронна
Практичний приклад: стрімінг пацієнтів з БД
Цей патерн — «стрімінг репозиторій» — є стандартом для роботи з великими наборами даних у сучасних .NET-застосунках. IAsyncEnumerable<T> підтримується у Entity Framework Core через ToAsyncEnumerable(), у gRPC-стрімінгу, у SignalR і у всій екосистемі .NET.