Когда конвертировать IEnumerable в IAsyncEnumerable

В документации .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?

Во втором примере ни один из этих методов на самом деле не является асинхронным. Ваш вопрос в основном заключается в том, в чем преимущество использования асинхронного режима при выполнении ввода-вывода?

ProgrammingLlama 06.04.2023 05:20

Я думаю, что я спрашиваю: этот вопрос действительно о IAsyncEnumerable или об асинхронном программировании в целом?

ProgrammingLlama 06.04.2023 05:23
IQueryable реализует слишком много интерфейсов, .AsAsyncEnumerable / .AsEnumerable просто приведите результат. В любом случае запрос будет выполнен, когда вы начнете перечислять результаты. Хотя это плохой пример, так как вы можете отфильтровать IQueryable и просто вернуть его.
Jeremy Lakeman 06.04.2023 06:33

Также streaming != async. Оба примера обрабатывают результаты по мере их получения. Но версия async не будет блокировать ваш поток, когда результат ожидает ввода-вывода.

Jeremy Lakeman 06.04.2023 06:35

@ProgrammingLlama в основном о том, как работает `await foreach` против IAsyncEnumerable. Я оставил комментарий под ответом Теодора, чтобы уточнить, помогает ли это вообще.

roverred 06.04.2023 08:48
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
5
207
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Преимущество 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 06.04.2023 08:46

@roverred Итак, когда я перебираю обычный List<int>, это на самом деле блокирующая операция ввода-вывода? << В этом случае операция ввода-вывода не выполняется. List<int> — это уже материализованный вид коллекции. Другими словами, всякий раз, когда вы пытаетесь получить следующий элемент, он уже будет там. Вам не нужно выполнять никаких вычислений, чтобы получить следующее значение.

Peter Csala 06.04.2023 09:03

@roverred Я добавил в ответ некоторый код, который может помочь вам понять, что происходит.

Theodor Zoulias 06.04.2023 09:33

По какой причине сначала нужно преобразовать в IAsyncEnumerable и выполнить ожидание для foreach?

Если вы хотите воспользоваться преимуществами асинхронного ввода-вывода. Всякий раз, когда вы извлекаете данные из базы данных в случае IEnumerable, это блокирующая операция. Вызывающий поток (ваш foreach) должен ждать, пока не придет ответ базы данных. В то время как в случае IAsyncEnumerable поток вызывающего абонента (ваш await foreach) может быть назначен другому запросу. Таким образом, он обеспечивает лучшую масштабируемость.

Это просто для упрощения синтаксиса или есть какие-то преимущества?

Если следующий элемент (скорее всего) еще недоступен, и вам нужно выполнить операцию ввода-вывода, чтобы получить его, вы можете тем временем освободить (в противном случае блокирующий) поток вызывающей стороны.

Есть ли преимущество в преобразовании любого IEnumerable в IAsyncEnumerable или только в том случае, если базовый IEnumerable также поддерживает потоковую передачу, например, через yield?

В вашем конкретном примере у вас есть два IAsyncEnumerables. Ваш источник данных и ваш http-ответ. Вам не обязательно использовать оба. Абсолютно нормально транслировать IEnumerable. Также можно асинхронно извлекать источник данных и возвращать результат всякий раз, когда доступны все данные.

Если у меня уже есть список, полностью загруженный в память, бессмысленно ли преобразовывать его в IAsyncEnumerable?

Что ж, если вы хотите передать ответ в потоковом режиме, то да, асинхронно извлекать источник данных не нужно.

Другие вопросы по теме