Executando várias tarefas assíncronas e aguardando a conclusão de todas elas

265

Preciso executar várias tarefas assíncronas em um aplicativo de console e aguardar a conclusão de todas elas antes de continuar o processamento.

Existem muitos artigos por aí, mas pareço ficar mais confuso quanto mais leio. Eu li e entendi os princípios básicos da biblioteca de tarefas, mas estou claramente perdendo um link em algum lugar.

Entendo que é possível encadear tarefas para que elas sejam iniciadas após a conclusão de outra (que é praticamente o cenário para todos os artigos que li), mas quero que todas as minhas tarefas sejam executadas ao mesmo tempo e que quero saber uma vez. estão todos concluídos.

Qual é a implementação mais simples para um cenário como este?

Daniel Minnaar
fonte

Respostas:

440

Ambas as respostas não mencionaram o esperado Task.WhenAll:

var task1 = DoWorkAsync();
var task2 = DoMoreWorkAsync();

await Task.WhenAll(task1, task2);

A principal diferença entre Task.WaitAlle Task.WhenAllé que o primeiro será bloqueado (semelhante ao uso Waitem uma única tarefa), enquanto o último não será e poderá ser aguardado, retornando o controle ao chamador até que todas as tarefas sejam concluídas.

Além disso, o tratamento de exceções é diferente:

Task.WaitAll:

Pelo menos uma das instâncias da tarefa foi cancelada - ou - uma exceção foi lançada durante a execução de pelo menos uma das instâncias da tarefa. Se uma tarefa foi cancelada, o AggregateException contém um OperationCanceledException em sua coleção InnerExceptions.

Task.WhenAll:

Se alguma das tarefas fornecidas for concluída em um estado com falha, a tarefa retornada também será concluída em um estado com falha, em que suas exceções conterão a agregação do conjunto de exceções não desempacotadas de cada uma das tarefas fornecidas.

Se nenhuma das tarefas fornecidas falhar, mas pelo menos uma delas foi cancelada, a tarefa retornada terminará no estado Cancelado.

Se nenhuma das tarefas falhar e nenhuma delas foi cancelada, a tarefa resultante terminará no estado RanToCompletion. Se a matriz / enumerável fornecida não contiver tarefas, a tarefa retornada passará imediatamente para um estado RanToCompletion antes de retornar ao chamador.

Yuval Itzchakov
fonte
4
Quando eu tento isso, minhas tarefas são executadas seqüencialmente? É necessário iniciar cada tarefa individualmente antes await Task.WhenAll(task1, task2);?
Zapnologica
4
@Zapnologica Task.WhenAllnão inicia as tarefas para você. Você precisa fornecê-los "quente", ou seja, já iniciado.
Yuval Itzchakov
2
Está bem. Isso faz sentido. Então, o que seu exemplo fará? Porque você não os iniciou?
Zapnologica
2
@YuvalItzchakov muito obrigado! É tão simples, mas me ajudou muito hoje! Vale pelo menos mil :)
Daniel Dušek
1
@ Pierre não estou seguindo. O que fazer StartNewe girar novas tarefas têm a ver com a espera assíncrona de todas elas?
Yuval Itzchakov 06/04
106

Você pode criar muitas tarefas como:

List<Task> TaskList = new List<Task>();
foreach(...)
{
   var LastTask = new Task(SomeFunction);
   LastTask.Start();
   TaskList.Add(LastTask);
}

Task.WaitAll(TaskList.ToArray());
Vírus
fonte
48
Eu recomendaria WhenAll
Ravi
É possível iniciar vários novos threads, ao mesmo tempo, usando a palavra-chave wait em vez de .Start ()?
Matt W
1
@ MattW Não, quando você usa o wait, ele espera que seja concluído. Nesse caso, você não seria capaz de criar um ambiente multithread. É por isso que todas as tarefas são aguardadas no final do loop.
Vírus
5
Votos negativos para futuros leitores, pois não ficou claro que se trata de uma chamada de bloqueio.
JRoughan
Veja a resposta aceita por motivos para não fazer isso.
EL MOJO
26

A melhor opção que eu vi é o seguinte método de extensão:

public static Task ForEachAsync<T>(this IEnumerable<T> sequence, Func<T, Task> action) {
    return Task.WhenAll(sequence.Select(action));
}

Chame assim:

await sequence.ForEachAsync(item => item.SomethingAsync(blah));

Ou com um lambda assíncrono:

await sequence.ForEachAsync(async item => {
    var more = await GetMoreAsync(item);
    await more.FrobbleAsync();
});
me22
fonte
26

Você pode usar o WhenAllque retornará um tipo de retorno aguardável Taskou WaitAllque não tem retorno e bloqueará a execução de código adicional semelhante Thread.Sleepaté que todas as tarefas sejam concluídas, canceladas ou com falha.

insira a descrição da imagem aqui

Exemplo

var tasks = new Task[] {
    TaskOperationOne(),
    TaskOperationTwo()
};

Task.WaitAll(tasks);
// or
await Task.WhenAll(tasks);

Se você deseja executar as tarefas em uma ordem prática, pode se inspirar nesta resposta.

NtFreX
fonte
desculpe-me por chegar atrasado à festa, mas por que você tem awaitpara cada operação e ao mesmo tempo usa WaitAllou WhenAll. As tarefas na Task[]inicialização não deveriam ficar sem await?
31 de
@dee zg Você está certo. A espera acima anula o objetivo. Vou mudar minha resposta e removê-los.
NtFreX 14/03/19
sim é isso. obrigado pela clarificação! (upvote para resposta Nice)
dee zg
8

Deseja encadear os Tasks ou eles podem ser chamados de maneira paralela?

Para encadear
Basta fazer algo como

Task.Run(...).ContinueWith(...).ContinueWith(...).ContinueWith(...);
Task.Factory.StartNew(...).ContinueWith(...).ContinueWith(...).ContinueWith(...);

e não se esqueça de verificar a Taskinstância anterior em cada uma, ContinueWithpois ela pode estar com defeito.

Pela maneira paralela
O método mais simples que me deparei: Parallel.Invoke Caso contrário, existe Task.WaitAllou você pode usar WaitHandles para fazer uma contagem regressiva para zero ações restantes (espere, há uma nova classe CountdownEvent:) ou ...

Andreas Niedermair
fonte
3
Aprecie a resposta, mas suas sugestões poderiam ter sido explicadas um pouco mais.
Daniel Minnaar 29/07
@drminnaar Que outra explicação, além dos links para o msdn, com exemplos, você precisa? você nem clicou nos links, clicou?
Andreas Niedermair
4
Eu cliquei nos links e li o conteúdo. Eu estava indo para o Invoke, mas havia muitos If e But sobre se é executado de forma assíncrona ou não. Você estava editando sua resposta continuamente. O link WaitAll que você postou foi perfeito, mas eu fui para a resposta que demonstrou a mesma funcionalidade de uma maneira mais rápida e fácil de ler. Não se ofenda, sua resposta ainda oferece boas alternativas para outras abordagens.
Daniel Minnaar 29/07
@drminnaar sem ofensa tomadas aqui, eu sou apenas um curioso :)
Andreas Niedermair
5

É assim que eu faço isso com uma matriz Func <> :

var tasks = new Func<Task>[]
{
   () => myAsyncWork1(),
   () => myAsyncWork2(),
   () => myAsyncWork3()
};

await Task.WhenAll(tasks.Select(task => task()).ToArray()); //Async    
Task.WaitAll(tasks.Select(task => task()).ToArray()); //Or use WaitAll for Sync
DalSoft
fonte
1
Por que você não a mantém como matriz de tarefas?
Talha Talip Açıkgöz
1
Se você não for cuidadoso, execute as tarefas quando não esperava que elas fossem executadas. Fazer isso como um delegado da Func deixa sua intenção clara.
21418 DalSoft
5

Mais uma resposta ... mas geralmente me encontro em um caso, quando preciso carregar dados simultaneamente e colocá-los em variáveis, como:

var cats = new List<Cat>();
var dog = new Dog();

var loadDataTasks = new Task[]
{
    Task.Run(async () => cats = await LoadCatsAsync()),
    Task.Run(async () => dog = await LoadDogAsync())
};

try
{
    await Task.WhenAll(loadDataTasks);
}
catch (Exception ex)
{
    // handle exception
}
Yehor Hromadskyi
fonte
1
Se LoadCatsAsync()e LoadDogAsync()são apenas chamadas de banco de dados, elas são vinculadas a IO. Task.Run()é para trabalho ligado à CPU; ele adiciona uma sobrecarga desnecessária adicional se tudo o que você está fazendo é aguardar uma resposta do servidor de banco de dados. A resposta aceita por Yuval é o caminho certo para o trabalho vinculado à IO.
Stephen Kennedy
@StephenKennedy, você poderia esclarecer que tipo de sobrecarga e quanto isso pode afetar o desempenho? Obrigado!
Yehor Hromadskyi 11/11/19
Seria muito difícil resumir na caixa de comentários :) Em vez disso, recomendo a leitura dos artigos de Stephen Cleary - ele é um especialista nessas coisas. Comece aqui: blog.stephencleary.com/2013/10/…
Stephen Kennedy
-1

Eu preparei um pedaço de código para mostrar como usar a tarefa para alguns desses cenários.

    // method to run tasks in a parallel 
    public async Task RunMultipleTaskParallel(Task[] tasks) {

        await Task.WhenAll(tasks);
    }
    // methode to run task one by one 
    public async Task RunMultipleTaskOneByOne(Task[] tasks)
    {
        for (int i = 0; i < tasks.Length - 1; i++)
            await tasks[i];
    }
    // method to run i task in parallel 
    public async Task RunMultipleTaskParallel(Task[] tasks, int i)
    {
        var countTask = tasks.Length;
        var remainTasks = 0;
        do
        {
            int toTake = (countTask < i) ? countTask : i;
            var limitedTasks = tasks.Skip(remainTasks)
                                    .Take(toTake);
            remainTasks += toTake;
            await RunMultipleTaskParallel(limitedTasks.ToArray());
        } while (remainTasks < countTask);
    }
sayah imad
fonte
1
como obter os resultados das tarefas? Por exemplo, para mesclar "linhas" (de N tarefas em paralelo) em uma tabela de dados e vinculá-la ao gridview asp.net?
PreguntonCojoneroCabrón 14/11/19