IAsyncEnumerable и await foreach
Асинхронно передавайте данные потоками с помощью IAsyncEnumerable, используйте yield в асинхронных итераторах и получайте данные через await foreach.
«IAsyncEnumerable и await foreach» — бесплатный урок C# Academy на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C# Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C# Academy содержит 4 уроков всего.
Зачем нужен IAsyncEnumerable?
IAsyncEnumerable<T> (представленный в C# 8 и .NET Core 3) обеспечивает асинхронную потоковую передачу: элементы выдаются по одному по мере готовности, без предварительной загрузки всего набора в память. Например, можно построчно читать курсор базы данных или потоково получать ответы API.
Написание асинхронного итератора
Объявите метод, возвращающий IAsyncEnumerable<T>, и используйте yield return внутри async-метода. Ключевые слова await и yield return естественно сочетаются.
public async IAsyncEnumerable<int> GenerateNumbersAsync(int count)
{
for (int i = 0; i < count; i++)
{
await Task.Delay(100); // simulate async work per item
yield return i;
}
}Использование await foreach
Обрабатывайте IAsyncEnumerable<T> с помощью await foreach. Каждая итерация асинхронно ожидает следующий элемент — consumer и producer естественным образом задают темп друг другу.
await foreach (var number in GenerateNumbersAsync(10))
{
Console.WriteLine(number);
}
// With cancellation:
var cts = new CancellationTokenSource();
await foreach (var n in GenerateNumbersAsync(100)
.WithCancellation(cts.Token))
{
if (n > 50) { cts.Cancel(); break; }
Console.WriteLine(n);
}Потоковое чтение из базы данных
EF Core поддерживает потоковое получение результатов запросов с помощью AsAsyncEnumerable(). Строки читаются по одной, без загрузки всего набора результатов в память — это особенно важно для больших таблиц.
public async IAsyncEnumerable<Product> StreamProductsAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var product in _db.Products
.Where(p => p.IsActive)
.AsAsyncEnumerable()
.WithCancellation(ct))
{
yield return product;
}
}Потоковая передача HTTP-ответов
Передавайте большие ответы клиентам без буферизации всего содержимого. Клиент получает данные постепенно, пока сервер продолжает обработку.
app.MapGet("/stream/products", (ProductRepository repo) =>
{
// Returns an IAsyncEnumerable — ASP.NET Core streams it as JSON array
return repo.StreamAllAsync();
});
// Or with custom serialization:
app.MapGet("/stream/events", async (HttpResponse response, EventStore store) =>
{
response.ContentType = "application/json";
await foreach (var e in store.StreamAsync())
{
await response.WriteAsJsonAsync(e);
await response.Body.FlushAsync();
}
});Атрибут EnumeratorCancellation
Примените [EnumeratorCancellation] к параметру CancellationToken асинхронного итератора, чтобы WithCancellation() на стороне consumer передавал этот токен producer.
public async IAsyncEnumerable<LogEntry> TailLogAsync(
string logFile,
[EnumeratorCancellation] CancellationToken ct = default)
{
while (!ct.IsCancellationRequested)
{
var newLines = await ReadNewLinesAsync(logFile, ct);
foreach (var line in newLines)
yield return new LogEntry(line);
await Task.Delay(500, ct);
}
}
// Consumer:
await foreach (var entry in TailLogAsync("/var/log/app.log")
.WithCancellation(cts.Token))
{
Console.WriteLine(entry.Message);
}LINQ для IAsyncEnumerable
Пакет NuGet System.Linq.Async предоставляет асинхронные операторы LINQ (WhereAwait, SelectAwait, ToListAsync) для IAsyncEnumerable.
// dotnet add package System.Linq.Async
var expensiveProducts = await repo.StreamAllAsync()
.WhereAwait(async p => await pricing.IsExpensiveAsync(p.Id))
.SelectAwait(async p => await enricher.EnrichAsync(p))
.Take(10)
.ToListAsync();
// Or use built-in Where/Select that don't need async:
var names = repo.StreamAllAsync()
.Where(p => p.IsActive)
.Select(p => p.Name);Объединение нескольких асинхронных потоков
Чтобы одновременно обрабатывать несколько асинхронных потоков, используйте Channel как точку объединения: несколько producers записывают данные, а один consumer их читает.
async IAsyncEnumerable<T> Merge<T>(
params IAsyncEnumerable<T>[] streams)
{
var channel = Channel.CreateUnbounded<T>();
var producers = streams.Select(async stream =>
{
await foreach (var item in stream)
await channel.Writer.WriteAsync(item);
});
_ = Task.WhenAll(producers)
.ContinueWith(_ => channel.Writer.Complete());
await foreach (var item in channel.Reader.ReadAllAsync())
yield return item;
}Производительность: буферизация и потоковая обработка
При обработке больших наборов данных всегда используйте потоковую обработку. Предварительная загрузка всего набора в список (ToListAsync()) одновременно удерживает все данные в памяти, тогда как потоковая обработка обрабатывает по одному элементу.
// BAD: loads all 1M records into memory
var allOrders = await _db.Orders.ToListAsync();
foreach (var order in allOrders)
await ProcessAsync(order);
// GOOD: processes one record at a time
await foreach (var order in _db.Orders.AsAsyncEnumerable())
await ProcessAsync(order);
// Memory usage: O(1) vs O(n)Обработка ошибок в асинхронных итераторах
Оберните весь await foreach в try/catch, чтобы обрабатывать ошибки producer. Ошибки итератора передаются consumer в текущей точке итерации.
try
{
await foreach (var item in RiskyStreamAsync())
{
await ProcessAsync(item);
}
}
catch (HttpRequestException ex)
{
Console.Error.WriteLine($"Stream error: {ex.Message}");
}
catch (OperationCanceledException)
{
Console.WriteLine("Stream cancelled.");
}Быстрая проверка
В чём главное преимущество IAsyncEnumerable перед возвратом List
Повторение: IAsyncEnumerable и await foreach
Основные выводы:
IAsyncEnumerable<T>: асинхронно передаёт элементы по одномуyield returnв async-методах создаёт асинхронные итераторыawait foreach: обрабатывает поток с отменой через.WithCancellation()[EnumeratorCancellation]: передаёт токен consumer в итератор- EF Core: используйте
AsAsyncEnumerable()для экономичной по памяти обработки больших запросов - При обработке больших наборов данных используйте потоковую передачу вместо
ToListAsync()
Часто задаваемые вопросы
Урок «IAsyncEnumerable и await foreach» бесплатный?
Да — полный текст урока «IAsyncEnumerable и await foreach» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C# Academy, подпишись на CoddyKit PRO. Курс C# Academy содержит 4 уроков всего.
Чему я научусь в уроке «IAsyncEnumerable и await foreach»?
Асинхронно передавайте данные потоками с помощью IAsyncEnumerable, используйте yield в асинхронных итераторах и получайте данные через await foreach. Ты практикуешь C# Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать C# Academy?
Предыдущий опыт не требуется. C# Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.
Сколько времени занимает урок «IAsyncEnumerable и await foreach»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке C# Academy?
Да. Каждый урок C# Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- IAsyncEnumerable и await foreach
- System.Threading.Channels
- ValueTask и предотвращение выделений памяти
- ConfigureAwait и контекст синхронизации