В документации .NET для типов возврата действий контроллера (ссылка на документ) показан пример возврата потока асинхронного ответа:
[HttpGet("asyncsale")]
public async IAsyncEnumerable<Product> GetOnSaleProductsAsync()
{
var products = _productContext.Products.OrderBy(p => p.Name).AsAsyncEnumerable();
await foreach (var product in products)
{
if (product.IsOnSale)
{
yield return product;
}
}
}
В приведенном выше примере _productContext.Products.OrderBy(p => p.Name).AsAsyncEnumerable() преобразует возвращенный IQueryable<Product> в IAsyncEnumerable. Но приведенный ниже пример также работает и передает ответ асинхронно.
[HttpGet("asyncsale")]
public async IAsyncEnumerable<Product> GetOnSaleProductsAsync()
{
var products = _productContext.Products.OrderBy(p => p.Name);
foreach (var product in products)
{
if (product.IsOnSale)
{
yield return product;
}
}
await Task.CompletedTask;
}
В чем причина того, что сначала нужно перейти на IAsyncEnumerable, а затем сделать await на foreach? Это просто для упрощения синтаксиса или есть какие-то преимущества?
Есть ли польза от преобразования любого IEnumerable в IAsyncEnumerable или только в том случае, если базовый IEnumerable также доступен для потоковой передачи, например, через yield? Если у меня уже есть список, полностью загруженный в память, бессмысленно ли преобразовывать его в IAsyncEnumerable?
Я думаю, что я спрашиваю: этот вопрос действительно о IAsyncEnumerable или об асинхронном программировании в целом?
IQueryable реализует слишком много интерфейсов, .AsAsyncEnumerable / .AsEnumerable просто приведите результат. В любом случае запрос будет выполнен, когда вы начнете перечислять результаты. Хотя это плохой пример, так как вы можете отфильтровать IQueryable и просто вернуть его.
Также streaming != async. Оба примера обрабатывают результаты по мере их получения. Но версия async не будет блокировать ваш поток, когда результат ожидает ввода-вывода.
@ProgrammingLlama в основном о том, как работает `await foreach` против IAsyncEnumerable. Я оставил комментарий под ответом Теодора, чтобы уточнить, помогает ли это вообще.





Преимущество IAsyncEnumerable<T> по сравнению с IEnumerable<T> заключается в том, что первый потенциально более масштабируем, потому что он не использует поток, пока он перечисляется. Вместо синхронного метода MoveNext у него есть асинхронный MoveNextAsync. Это преимущество становится спорным вопросом, когда MoveNextAsync всегда возвращает уже завершенное ValueTask<bool> (enumerator.MoveNextAsync().IsCompleted == true), и в этом случае у вас есть только синхронное перечисление, замаскированное под асинхронное. В этом случае нет преимущества масштабируемости. Именно это и происходит в коде, показанном в вопросе. У вас есть шасси Porsche с двигателем Trabant, спрятанным под капотом.
Если вы хотите получить более глубокое понимание происходящего, вы можете перебрать асинхронную последовательность вручную вместо удобного await foreach и собрать отладочную информацию по каждому шагу перебора:
[HttpGet("asyncsale")]
public async IAsyncEnumerable<Product> GetOnSaleProductsAsync()
{
var products = _productContext.Products.OrderBy(p => p.Name).AsAsyncEnumerable();
Stopwatch stopwatch = new();
await using IAsyncEnumerator<Product> enumerator = products.GetAsyncEnumerator();
while (true)
{
stopwatch.Restart();
ValueTask<bool> moveNextTask = enumerator.MoveNextAsync();
TimeSpan elapsed1 = stopwatch.Elapsed;
bool isCompleted = moveNextTask.IsCompleted;
stopwatch.Restart();
bool moved = await moveNextTask;
TimeSpan elapsed2 = stopwatch.Elapsed;
Console.WriteLine($"Create: {elapsed1}, Completed: {isCompleted}, Await: {elapsed2}");
if (!moved) break;
Product product = enumerator.Current;
if (product.IsOnSale)
{
yield return product;
}
}
}
Скорее всего, вы обнаружите, что все MoveNextAsync операции завершаются при создании, по крайней мере, некоторые из них имеют значимое elapsed1 значение, и все они имеют нулевое elapsed2 значение.
Означает ли это, что всякий раз, когда что-то преобразуется в IAsyncEnumerable, у него будет асинхронный итератор? Асинхронная итерация - это то, где я больше всего запутался. Итак, когда я повторяю обычный List<int>, это на самом деле блокирующая операция ввода-вывода? Но если я преобразую List<int> в IAsyncEnumerable, итерация станет неблокирующей асинхронной операцией? Мое предположение заключалось в том, что для фактической async итерации базовые IEnumerable данные должны быть получены async способом и должны быть итерируемыми без однократного извлечения всех данных.
@roverred Итак, когда я перебираю обычный List<int>, это на самом деле блокирующая операция ввода-вывода? << В этом случае операция ввода-вывода не выполняется. List<int> — это уже материализованный вид коллекции. Другими словами, всякий раз, когда вы пытаетесь получить следующий элемент, он уже будет там. Вам не нужно выполнять никаких вычислений, чтобы получить следующее значение.
@roverred Я добавил в ответ некоторый код, который может помочь вам понять, что происходит.
По какой причине сначала нужно преобразовать в IAsyncEnumerable и выполнить ожидание для foreach?
Если вы хотите воспользоваться преимуществами асинхронного ввода-вывода. Всякий раз, когда вы извлекаете данные из базы данных в случае IEnumerable, это блокирующая операция. Вызывающий поток (ваш foreach) должен ждать, пока не придет ответ базы данных. В то время как в случае IAsyncEnumerable поток вызывающего абонента (ваш await foreach) может быть назначен другому запросу. Таким образом, он обеспечивает лучшую масштабируемость.
Это просто для упрощения синтаксиса или есть какие-то преимущества?
Если следующий элемент (скорее всего) еще недоступен, и вам нужно выполнить операцию ввода-вывода, чтобы получить его, вы можете тем временем освободить (в противном случае блокирующий) поток вызывающей стороны.
Есть ли преимущество в преобразовании любого IEnumerable в IAsyncEnumerable или только в том случае, если базовый IEnumerable также поддерживает потоковую передачу, например, через yield?
В вашем конкретном примере у вас есть два IAsyncEnumerables. Ваш источник данных и ваш http-ответ. Вам не обязательно использовать оба. Абсолютно нормально транслировать IEnumerable. Также можно асинхронно извлекать источник данных и возвращать результат всякий раз, когда доступны все данные.
Если у меня уже есть список, полностью загруженный в память, бессмысленно ли преобразовывать его в IAsyncEnumerable?
Что ж, если вы хотите передать ответ в потоковом режиме, то да, асинхронно извлекать источник данных не нужно.
Во втором примере ни один из этих методов на самом деле не является асинхронным. Ваш вопрос в основном заключается в том, в чем преимущество использования асинхронного режима при выполнении ввода-вывода?