Como esperar uma lista de tarefas de maneira assíncrona usando LINQ?

87

Eu tenho uma lista de tarefas que criei assim:

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

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

    ...
}

Ao usar .ToList(), todas as tarefas devem ser iniciadas. Agora quero aguardar sua finalização e retornar os resultados.

Isso funciona no ...bloco acima :

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

Ele faz o que eu quero, mas parece um tanto desajeitado. Prefiro escrever algo mais simples como este:

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

... mas isso não compila. o que estou perdendo? Ou simplesmente não é possível expressar as coisas dessa maneira?

Matt Johnson-Pint
fonte
Você precisa processar em DoSomethingAsync(foo)série para cada foo ou este é um candidato para Parallel.ForEach <Foo> ?
mdisibio
1
@mdisibio - Parallel.ForEachestá bloqueando. O padrão aqui vem do vídeo Asynchronous C # de Jon Skeet no Pluralsight . Executa em paralelo sem bloqueio.
Matt Johnson-Pint
@mdisibio - Não. Eles correm em paralelo. Experimente . (Além disso, parece que não preciso .ToList()se for apenas usar WhenAll.)
Matt Johnson-Pint
Ponto tomado. Dependendo de como DoSomethingAsyncé escrita, a lista pode ou não ser executada em paralelo. Consegui escrever um método de teste que era e uma versão que não era, mas em ambos os casos o comportamento é ditado pelo próprio método, não pelo delegado que cria a tarefa. Desculpe pela confusão. Porém, se DoSomethingAsycretornar Task<Foo>, então o awaitin no delegado não é absolutamente necessário ... Acho que esse era o ponto principal que eu ia tentar fazer.
mdisibio

Respostas:

136

LINQ não funciona perfeitamente com asynccódigo, mas você pode fazer isso:

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

Se todas as suas tarefas retornarem o mesmo tipo de valor, você pode até fazer isso:

var results = await Task.WhenAll(tasks);

o que é muito bom. WhenAllretorna uma matriz, então acredito que seu método pode retornar os resultados diretamente:

return await Task.WhenAll(tasks);
Stephen Cleary
fonte
11
Só queria salientar que isso também pode funcionar comvar tasks = foos.Select(foo => DoSomethingAsync(foo)).ToList();
mdisibio
1
ou mesmovar tasks = foos.Select(DoSomethingAsync).ToList();
Todd Menier de
3
qual é a razão por trás disso que o Linq não funciona perfeitamente com código assíncrono?
Ehsan Sajjad
2
@EhsanSajjad: Porque LINQ to Objects funciona sincronizadamente em objetos na memória. Algumas coisas limitadas funcionam, como Select. Mas a maioria não gosta Where.
Stephen Cleary
4
@EhsanSajjad: Se a operação for baseada em E / S, você pode usar asyncpara reduzir threads; se estiver vinculado à CPU e já estiver em um thread de segundo plano, asyncnão fornecerá nenhum benefício.
Stephen Cleary
9

Para expandir a resposta de Stephen, criei o seguinte método de extensão para manter o estilo fluente do LINQ. Você pode então fazer

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);
        }
    }
}
Clemente
fonte
10
Pessoalmente, eu ToArrayAsync
nomearia
4

Um problema com Task.WhenAll é que ele criaria um paralelismo. Na maioria dos casos, pode ser ainda melhor, mas às vezes você deseja evitar. Por exemplo, ler dados em lotes do banco de dados e enviar dados para algum serviço remoto da web. Você não deseja carregar todos os lotes para a memória, mas acessar o banco de dados assim que o lote anterior for processado. Então, você tem que quebrar a assincronicidade. Aqui está um exemplo:

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)
{
}

Observação .GetAwaiter (). GetResult () convertendo-o em synchronose. O DB seria atingido lentamente apenas depois que batchSize de eventos fosse processado.

Boris Lipschitz
fonte
1

Use Task.WaitAllou o Task.WhenAllque for apropriado.

LIBRA
fonte
1
Isso também não funciona. Task.WaitAllestá bloqueando, não está aguardando e não funcionará com a Task<T>.
Matt Johnson-Pint
@MattJohnson WhenAll?
LB
Sim. É isso aí! Eu me sinto estúpido. Obrigado!
Matt Johnson-Pint
0

Task.WhenAll deve fazer o truque aqui.

Ameen
fonte