Async ждут в linq select

181

Мне нужно изменить существующую программу, и она содержит следующий код:

var inputs = events.Select(async ev => await ProcessEventAsync(ev))
                   .Select(t => t.Result)
                   .Where(i => i != null)
                   .ToList();

Но это кажется мне очень странным, в первую очередь использование asyncи awaitв select. Согласно этому ответу Стивена Клири, я смогу отбросить их.

Затем второй, Selectкоторый выбирает результат. Не означает ли это, что задача вообще не асинхронная и выполняется синхронно (столько усилий ни за что), или задача будет выполняться асинхронно, и когда она будет выполнена, остальная часть запроса будет выполнена?

Должен ли я написать приведенный выше код следующим образом в соответствии с другим ответом Стивена Клири :

var tasks = await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev)));
var inputs = tasks.Where(result => result != null).ToList();

и это абсолютно так же, как это?

var inputs = (await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev))))
                                       .Where(result => result != null).ToList();

Пока я работаю над этим проектом, я хотел бы изменить первый пример кода, но я не слишком заинтересован в изменении (явно работающем) асинхронного кода. Может быть, я просто беспокоюсь, и все 3 примера кода делают одно и то же?

ProcessEventsAsync выглядит так:

async Task<InputResult> ProcessEventAsync(InputEvent ev) {...}
Александр Дерк
источник
Какой тип возврата ProceesEventAsync?
tede24
@ tede24 Это Task<InputResult>с InputResultтого , чтобы быть пользовательский класс.
Александр Дерк
Ваши версии намного легче читать по моему мнению. Тем не менее, вы забыли Selectрезультаты заданий перед вашими Where.
Макс
И InputResult имеет право собственности Result?
tede24
@ tede24 Результат - это свойство задачи, а не моего класса. И @Max ожидающий должен убедиться, что я получаю результаты без доступа к Resultсвойству задачи
Александр Дерк

Ответы:

185
var inputs = events.Select(async ev => await ProcessEventAsync(ev))
                   .Select(t => t.Result)
                   .Where(i => i != null)
                   .ToList();

Но это кажется мне очень странным, в первую очередь использование async и await в select. Согласно этому ответу Стивена Клири, я смогу отбросить их.

Звонок Selectдействителен. Эти две строки практически идентичны:

events.Select(async ev => await ProcessEventAsync(ev))
events.Select(ev => ProcessEventAsync(ev))

(Существует небольшая разница в том, как генерируется синхронное исключение ProcessEventAsync, но в контексте этого кода это не имеет значения).

Затем второй выбор, который выбирает результат. Разве это не означает, что задача вообще не асинхронная и выполняется синхронно (столько усилий ни за что), или задача будет выполняться асинхронно, и когда она будет выполнена, остальная часть запроса будет выполнена?

Это означает, что запрос блокируется. Так что это не совсем асинхронно.

Разбивая это:

var inputs = events.Select(async ev => await ProcessEventAsync(ev))

сначала запустит асинхронную операцию для каждого события. Тогда эта строка:

                   .Select(t => t.Result)

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

Это та часть, которая меня не волнует, потому что она блокирует, а также оборачивает любые исключения AggregateException.

и это абсолютно так же, как это?

var tasks = await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev)));
var inputs = tasks.Where(result => result != null).ToList();

var inputs = (await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev))))
                                       .Where(result => result != null).ToList();

Да, эти два примера эквивалентны. Они оба запускают все асинхронные операции ( events.Select(...)), затем асинхронно ждут завершения всех операций в любом порядке ( await Task.WhenAll(...)), а затем приступают к остальной части работы ( Where...).

Оба эти примера отличаются от исходного кода. Исходный код блокирует и будет заключать исключения в AggregateException.

Стивен Клири
источник
Приветствия для прояснения этого! Таким образом, вместо исключений, заключенных в, AggregateExceptionя получу несколько отдельных исключений во втором коде?
Александр Дерк
1
@AlexanderDerck: Нет, и в старом, и в новом коде возникает только первое исключение. Но с Resultэтим было бы завернуто AggregateException.
Стивен Клири
Я получаю тупик в моем ASP.NET MVC Controller с помощью этого кода. Я решил это с помощью Task.Run (…). У меня нет хорошего чувства по этому поводу. Тем не менее, он завершился в самый раз при запуске асинхронного теста xUnit. В чем дело?
SuperJMN
2
@SuperJMN: Заменить stuff.Select(x => x.Result);наawait Task.WhenAll(stuff)
Стивен Клири
1
@DanielS: они по сути одинаковы. Существуют некоторые различия, такие как конечные автоматы, контекст захвата, поведение синхронных исключений. Больше информации на blog.stephencleary.com/2016/12/eliding-async-await.html
Стивен Клири
25

Существующий код работает, но блокирует поток.

.Select(async ev => await ProcessEventAsync(ev))

создает новую задачу для каждого события, но

.Select(t => t.Result)

блокирует поток, ожидающий завершения каждой новой задачи.

С другой стороны, ваш код дает тот же результат, но остается асинхронным.

Только один комментарий к вашему первому коду. Эта линия

var tasks = await Task.WhenAll(events...

создаст одну задачу, поэтому переменная должна быть названа в единственном числе.

Наконец ваш последний код делает то же самое, но более кратким

Для справки: Task.Wait / Task.WhenAll

tede24
источник
Таким образом, первый блок кода фактически выполняется синхронно?
Александр Дерк
1
Да, потому что доступ к Result приводит к ожиданию, которое блокирует поток. С другой стороны, когда производит новую задачу, вы можете ждать.
tede24
1
Возвращаясь к этому вопросу и глядя на ваше замечание о названии tasksпеременной, вы совершенно правы. Ужасный выбор, они даже не задачи, так как их ждут сразу. Я просто оставлю вопрос как есть
Александр Дерк
13

С текущими методами, доступными в Linq, это выглядит довольно уродливо:

var tasks = items.Select(
    async item => new
    {
        Item = item,
        IsValid = await IsValid(item)
    });
var tuples = await Task.WhenAll(tasks);
var validItems = tuples
    .Where(p => p.IsValid)
    .Select(p => p.Item)
    .ToList();

Надеемся, что следующие версии .NET предложат более элегантный инструментарий для обработки коллекций задач и задач коллекций.

Виталий Улантиков
источник
13

Я использовал этот код:

public static async Task<IEnumerable<TResult>> SelectAsync<TSource,TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> method)
{
      return await Task.WhenAll(source.Select(async s => await method(s)));
}

как это:

var result = await sourceEnumerable.SelectAsync(async s=>await someFunction(s,other params));
Сидерит Zackwehdex
источник
5
Это просто оборачивает существующую функциональность более непонятным образом
Александр Дерк
Альтернативой является var result = await Task.WhenAll (sourceEnumerable.Select (async s => await someFunction (s, other params)). Это тоже работает, но не LINQy
Siderite Zackwehdex
2
Дополнительные параметры являются внешними, в зависимости от функции, которую я хочу выполнить, они не имеют значения в контексте метода расширения.
Siderite Zackwehdex
5
Это прекрасный метод расширения. Не уверен, почему это было сочтено «более неясным» - это семантически аналогично синхронному Select(), так что это элегантное раскрытие.
nullPainter
1
asyncИ awaitвнутри первой лямбды является излишним. Метод SelectAsync можно просто записать так:return await Task.WhenAll(source.Select(method));
Натан,
12

Я предпочитаю это как метод расширения:

public static async Task<IEnumerable<T>> WhenAll<T>(this IEnumerable<Task<T>> tasks)
{
    return await Task.WhenAll(tasks);
}

Так что это можно использовать с цепочкой метода:

var inputs = await events
  .Select(async ev => await ProcessEventAsync(ev))
  .WhenAll()
Дэрил
источник
1
Вы не должны вызывать метод, Waitкогда он на самом деле не ждет. Это создание задачи, которая завершается, когда все задачи выполнены. Назовите это WhenAll, как Taskметод, который он эмулирует. Это также бессмысленно для метода async. Просто позвоните WhenAllи покончите с этим.
Сервировка
Немного бесполезной обертки на мой взгляд, когда он просто вызывает оригинальный метод
Александр Дерк
@ Серьезно, но мне не особо нравятся какие-либо варианты названий. Когда все это звучит как событие, которое это не совсем.
Дэрил
3
@AlexanderDerck преимущество в том, что вы можете использовать его в цепочке методов.
Дэрил
2
@Daryl, поскольку WhenAllвозвращает оцененный список (он не оценивается лениво), можно указать аргумент, чтобы использовать Task<T[]>возвращаемый тип для обозначения этого. Когда ожидается, он все еще сможет использовать Linq, но также сообщит, что он не ленив.
JAD