Как асинхронно ожидать список задач с помощью LINQ?

87

У меня есть список задач, которые я создал вот так:

public async Task<IList<Foo>> GetFoosAndDoSomethingAsync()
{
    var foos = await GetFoosAsync();

    var tasks = foos.Select(async foo => await DoSomethingAsync(foo)).ToList();

    ...
}

При использовании .ToList()все задачи должны запускаться. Теперь хочу дождаться их завершения и вернуть результаты.

Это работает в приведенном выше ...блоке:

var list = new List<Foo>();
foreach (var task in tasks)
    list.Add(await task);
return list;

Он делает то, что я хочу, но это кажется довольно неуклюжим. Лучше я напишу что-нибудь попроще:

return tasks.Select(async task => await task).ToList();

... но это не компилируется. Что мне не хватает? Или это просто невозможно так выразить?

Мэтт Джонсон-Пинт
источник
Вам нужно DoSomethingAsync(foo)последовательно обрабатывать каждый foo, или это кандидат на Parallel.ForEach <Foo> ?
mdisibio
1
@mdisibio - Parallel.ForEachэто блокировка. Шаблон здесь взят из видео Джона Скита об асинхронном C # на Pluralsight . Он выполняется параллельно без блокировки.
Мэтт Джонсон-Пинт
@mdisibio - Нет. Они работают параллельно. Попробуй . (Вдобавок, похоже, мне не нужно, .ToList()если я просто собираюсь использовать WhenAll.)
Мэтт Джонсон-Пинт
Дело принято. В зависимости от того, как DoSomethingAsyncнаписано, список может выполняться или не выполняться параллельно. Я смог написать тестовый метод, который был, и версию, которой не было, но в любом случае поведение продиктовано самим методом, а не делегатом, создавшим задачу. Извините за путаницу. Однако, если DoSomethingAsycвозвращается Task<Foo>, то awaitin the delegate не является абсолютно необходимым ... Я думаю, что это было основным моментом, который я собирался попытаться высказать.
mdisibio

Ответы:

136

LINQ плохо работает с asyncкодом, но вы можете сделать это:

var tasks = foos.Select(DoSomethingAsync).ToList();
await Task.WhenAll(tasks);

Если все ваши задачи возвращают один и тот же тип значения, вы даже можете сделать это:

var results = await Task.WhenAll(tasks);

что довольно приятно. WhenAllвозвращает массив, поэтому я считаю, что ваш метод может возвращать результаты напрямую:

return await Task.WhenAll(tasks);
Стивен Клири
источник
11
Просто хотел отметить, что это также может работать сvar tasks = foos.Select(foo => DoSomethingAsync(foo)).ToList();
mdisibio
1
или дажеvar tasks = foos.Select(DoSomethingAsync).ToList();
Тодд Менье
3
в чем причина того, что Linq не работает идеально с асинхронным кодом?
Эхсан Саджад
2
@EhsanSajjad: потому что LINQ to Objects синхронно работает с объектами в памяти. Некоторые ограниченные вещи работают, например Select. Но большинству не нравится Where.
Стивен Клири
4
@EhsanSajjad: если операция основана на вводе-выводе, то вы можете использовать asyncдля сокращения потоков; если он привязан к процессору и уже находится в фоновом потоке, то asyncне принесет никакой пользы.
Стивен Клири
9

Чтобы расширить ответ Стивена, я создал следующий метод расширения, чтобы сохранить свободный стиль LINQ. Затем вы можете сделать

await someTasks.WhenAll()

namespace System.Linq
{
    public static class IEnumerableExtensions
    {
        public static Task<T[]> WhenAll<T>(this IEnumerable<Task<T>> source)
        {
            return Task.WhenAll(source);
        }
    }
}
Климент
источник
10
Лично я бы назвал ваш метод расширенияToArrayAsync
Торвин
4

Одна проблема с Task.WhenAll заключается в том, что он создает параллелизм. В большинстве случаев это может быть даже лучше, но иногда вы хотите этого избежать. Например, пакетное чтение данных из БД и отправка данных в какой-то удаленный веб-сервис. Вы не хотите загружать все пакеты в память, а попадете в БД после обработки предыдущего пакета. Итак, вы должны нарушить асинхронность. Вот пример:

var events = Enumerable.Range(0, totalCount/ batchSize)
   .Select(x => x*batchSize)
   .Select(x => dbRepository.GetEventsBatch(x, batchSize).GetAwaiter().GetResult())
   .SelectMany(x => x);
foreach (var carEvent in events)
{
}

Обратите внимание, что .GetAwaiter (). GetResult () преобразует его в синхронизацию. БД будет обработано лениво только после обработки batchSize событий.

Борис Липшиц
источник
1

Используйте Task.WaitAllили в Task.WhenAllзависимости от того, что вам нужно.

ФУНТ
источник
1
Это тоже не работает. Task.WaitAllблокирует, не ожидает и не работает с Task<T>.
Мэтт Джонсон-Пинт
@MattJohnson WhenAll?
LB
Ага. Это оно! Я чувствую себя тупым. Благодарность!
Мэтт Джонсон-Пинт
0

Task.WhenAll должен помочь здесь.

Амин
источник