Aguardando várias tarefas com resultados diferentes

237

Eu tenho 3 tarefas:

private async Task<Cat> FeedCat() {}
private async Task<House> SellHouse() {}
private async Task<Tesla> BuyCar() {}

Todos eles precisam ser executados antes que meu código possa continuar e eu também preciso dos resultados de cada um. Nenhum dos resultados tem algo em comum entre si

Como ligo e espero que as 3 tarefas sejam concluídas e depois obtenho os resultados?

Ian Vink
fonte
25
Você tem algum requisito de pedido? Ou seja, você quer vender a casa até depois que o gato é alimentado?
precisa

Respostas:

411

Depois de usar WhenAll, você pode obter os resultados individualmente com await:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Você também pode usá-lo Task.Result(já que, nesse ponto, todos eles foram concluídos com êxito). No entanto, eu recomendo o uso, awaitporque está claramente correto, enquanto Resultpode causar problemas em outros cenários.

Stephen Cleary
fonte
83
Você pode simplesmente remover WhenAllcompletamente isso; os esperas cuidam para garantir que você não passe das três atribuições posteriores até que as tarefas sejam concluídas.
Servy
134
Task.WhenAll()permite executar a tarefa no modo paralelo . Não consigo entender por que o @Servy sugeriu removê-lo. Sem o WhenAllque será executado um por um
Sergey G.
87
@ Emery: As tarefas começam a executar imediatamente. Por exemplo, catTaskjá está em execução no momento em que é retornado FeedCat. Portanto, qualquer awaituma dessas abordagens funcionará - a única questão é se você deseja que elas sejam uma de cada vez ou todas juntas. O tratamento de erros é um pouco diferente - se você usar Task.WhenAll, awaittodos eles serão executados, mesmo que um deles falhe mais cedo.
Stephen Cleary
23
O @Sergey Calling WhenAllnão afeta quando as operações são executadas ou como são executadas. Ele tem possibilidade de afetar como os resultados são observados. Nesse caso em particular, a única diferença é que um erro em um dos dois primeiros métodos resultaria na exceção ser lançada nessa pilha de chamadas mais cedo no meu método do que na de Stephen (embora o mesmo erro sempre seja gerado, se houver algum )
Servy
37
@ Emery: A chave é que métodos assíncronos sempre retornam tarefas "quentes" (já iniciadas).
Stephen Cleary
99

Apenas awaitas três tarefas separadamente, depois de iniciar todas elas.

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;
Servy
fonte
8
@ Bargitta Não, isso é falso. Eles farão seu trabalho em paralelo. Sinta-se livre para executá-lo e ver por si mesmo.
Servy
5
Pessoas manter a mesma pergunta depois de anos ... Eu sinto que é importante ressaltar mais uma vez que uma tarefa " começa em criar " no corpo da resposta : talvez eles não se incomodam de ler comentários
9
@StephenYork Adicionando Task.WhenAllalterações literalmente nada sobre o comportamento do programa, de qualquer maneira observável. É uma chamada de método puramente redundante. Você pode adicioná-lo, se quiser, como uma opção estética, mas isso não muda o que o código faz. O tempo de execução do código será idêntico ou não à chamada de método (bem, tecnicamente haverá uma sobrecarga muito pequena para a chamada WhenAll, mas isso deve ser insignificante), apenas tornando a versão um pouco mais demorada do que esta versão.
Servy
4
@StephenYork Seu exemplo executa as operações sequencialmente por dois motivos. Seus métodos assíncronos não são realmente assíncronos, são síncronos. O fato de você ter métodos síncronos que sempre retornam tarefas já concluídas impede a execução simultânea. Em seguida, você realmente não fazer o que é mostrado nesta resposta de começar todos os três métodos assíncronos, e , em seguida, à espera das três tarefas, por sua vez. Seu exemplo não chama cada método até que o anterior termine, impedindo explicitamente que um seja iniciado até que o anterior termine, diferentemente desse código.
Servy
4
@MarcvanNieuwenhuijzen Provavelmente isso não é verdade, como foi discutido nos comentários aqui e em outras respostas. Adicionar WhenAllé uma mudança puramente estética. A única diferença observável no comportamento é se você espera que as tarefas posteriores sejam concluídas se uma tarefa anterior falhar, o que normalmente não é necessário. Se você não acredita nas inúmeras explicações sobre por que sua afirmação não é verdadeira, basta executar o código por conta própria e verificar que não é verdade.
Servy
37

Se você estiver usando o C # 7, poderá usar um método prático de wrapper como este ...

public static class TaskEx
{
    public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
    {
        return (await task1, await task2);
    }
}

... para ativar uma sintaxe conveniente como esta quando você desejar aguardar várias tarefas com diferentes tipos de retorno. Você teria que fazer várias sobrecargas para diferentes números de tarefas, é claro.

var (someInt, someString) = await TaskEx.WhenAll(GetIntAsync(), GetStringAsync());

No entanto, consulte a resposta de Marc Gravell para algumas otimizações em torno do ValueTask e tarefas já concluídas, se você pretende transformar este exemplo em algo real.

Joel Mueller
fonte
Tuplas são o único recurso C # 7 envolvido aqui. Esses estão definitivamente no lançamento final.
Joel Mueller
Eu sei sobre tuplas ec # 7. Quero dizer, não consigo encontrar o método WhenAll, que retorna tuplas. Qual namespace / pacote?
Yury Scherbakov
@YuryShcherbakov Task.WhenAll()não está retornando uma tupla. Um está sendo construído a partir das Resultpropriedades das tarefas fornecidas após a conclusão da tarefa retornada Task.WhenAll().
precisa saber é o seguinte
2
Sugiro substituir as .Resultchamadas de acordo com o raciocínio de Stephen para evitar que outras pessoas perpetuem a má prática, copiando seu exemplo.
precisa saber é
Eu me pergunto por que esse método não faz parte do quadro? Parece tão útil. Eles ficaram sem tempo e tiveram que parar com um único tipo de retorno?
Ian Grainger
14

Dadas três tarefas - FeedCat(), SellHouse()e BuyCar(), existem dois casos interessantes: todos eles são concluídos de forma síncrona (por algum motivo, talvez cache ou erro), ou não.

Digamos que temos, a partir da pergunta:

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();
    // what here?
}

Agora, uma abordagem simples seria:

Task.WhenAll(x, y, z);

mas ... isso não é conveniente para processar os resultados; nós normalmente queremos awaitisso:

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    await Task.WhenAll(x, y, z);
    // presumably we want to do something with the results...
    return DoWhatever(x.Result, y.Result, z.Result);
}

mas isso gera muita sobrecarga e aloca várias matrizes (incluindo a params Task[]matriz) e listas (internamente). Funciona, mas não é ótimo IMO. De muitas maneiras, é mais simples usar uma asyncoperação e apenas awaituma por vez:

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    // do something with the results...
    return DoWhatever(await x, await y, await z);
}

Ao contrário de alguns dos comentários acima, usar em awaitvez de nãoTask.WhenAll faz diferença na maneira como as tarefas são executadas (simultaneamente, sequencialmente, etc.). No nível mais alto, Task.WhenAll antecede o bom suporte do compilador para async/ awaite foi útil quando essas coisas não existiam . Também é útil quando você tem uma variedade arbitrária de tarefas, em vez de três tarefas discretas.

Mas: ainda temos o problema que async/ awaitgera muito ruído do compilador para a continuação. Se é provável que as tarefas pode realmente completar de forma síncrona, então podemos otimizar isso através da construção de um caminho síncrona com um fallback assíncrona:

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    if(x.Status == TaskStatus.RanToCompletion &&
       y.Status == TaskStatus.RanToCompletion &&
       z.Status == TaskStatus.RanToCompletion)
        return Task.FromResult(
          DoWhatever(a.Result, b.Result, c.Result));
       // we can safely access .Result, as they are known
       // to be ran-to-completion

    return Awaited(x, y, z);
}

async Task Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
    return DoWhatever(await x, await y, await z);
}

Essa abordagem "caminho de sincronização com fallback assíncrono" é cada vez mais comum, especialmente em código de alto desempenho, onde conclusões síncronas são relativamente frequentes. Observe que não ajudará em nada se a conclusão for sempre genuinamente assíncrona.

Coisas adicionais que se aplicam aqui:

  1. com o C # recente, um padrão comum é o asyncmétodo de fallback geralmente implementado como uma função local:

    Task<string> DoTheThings() {
        async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
            return DoWhatever(await a, await b, await c);
        }
        Task<Cat> x = FeedCat();
        Task<House> y = SellHouse();
        Task<Tesla> z = BuyCar();
    
        if(x.Status == TaskStatus.RanToCompletion &&
           y.Status == TaskStatus.RanToCompletion &&
           z.Status == TaskStatus.RanToCompletion)
            return Task.FromResult(
              DoWhatever(a.Result, b.Result, c.Result));
           // we can safely access .Result, as they are known
           // to be ran-to-completion
    
        return Awaited(x, y, z);
    }
  2. preferem ValueTask<T>para Task<T>se há uma boa chance das coisas nunca completamente síncrona com muitos valores de retorno diferentes:

    ValueTask<string> DoTheThings() {
        async ValueTask<string> Awaited(ValueTask<Cat> a, Task<House> b, Task<Tesla> c) {
            return DoWhatever(await a, await b, await c);
        }
        ValueTask<Cat> x = FeedCat();
        ValueTask<House> y = SellHouse();
        ValueTask<Tesla> z = BuyCar();
    
        if(x.IsCompletedSuccessfully &&
           y.IsCompletedSuccessfully &&
           z.IsCompletedSuccessfully)
            return new ValueTask<string>(
              DoWhatever(a.Result, b.Result, c.Result));
           // we can safely access .Result, as they are known
           // to be ran-to-completion
    
        return Awaited(x, y, z);
    }
  3. se possível, preferem IsCompletedSuccessfullya Status == TaskStatus.RanToCompletion; isso agora existe no .NET Core para Taske em qualquer lugar paraValueTask<T>

Marc Gravell
fonte
"Ao contrário de várias respostas aqui, usar aguardar em vez de Task.WhenAll não faz diferença na maneira como as tarefas são executadas (simultaneamente, sequencialmente, etc)". Não vejo nenhuma resposta que diga isso. Eu já teria comentado sobre eles dizendo o mesmo se o fizessem. Há muitos comentários sobre muitas respostas dizendo isso, mas nenhuma resposta. A que você está se referindo? Observe também que sua resposta não lida com o resultado das tarefas (ou lida com o fato de que os resultados são de um tipo diferente). Você os compôs em um método que apenas retorna a Taskquando tudo é feito sem usar os resultados.
Servy
@ Service, você está certo, isso foi comentários; Vou adicionar um tweak para mostrar usando os resultados.
Marc Gravell
@Servy tweak added
Marc Gravell
Além disso, se você optar por não executar tarefas síncronas antecipadamente, também poderá lidar com todas as tarefas canceladas ou com falha síncrona, em vez de apenas aquelas concluídas com êxito. Se você tomou a decisão de que é uma otimização de que o seu programa precisa (o que será raro, mas acontecerá), é melhor você seguir em frente.
Servy
@Servy que é um tópico complexo - você obtém semânticas de exceção diferentes dos dois cenários - aguardar para acionar uma exceção se comporta de maneira diferente do que acessar .Result para acionar a exceção. Na IMO, nesse ponto, devemos awaitobter a semântica "melhor" das exceções, supondo que as exceções sejam raras, mas significativas.
Marc Gravell
12

Você pode armazená-los em tarefas e aguardar todos:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

Cat cat = await catTask;
House house = await houseTask;
Car car = await carTask;
Reed Copsey
fonte
não var catTask = FeedCat()executa a função FeedCat()e armazena o resultado para catTasktornar a await Task.WhenAll()parte meio inútil, pois o método já foi executado?
Kraang Prime
1
@sanuel se voltarem tarefa <t>, então não ... eles começam a assíncrono aberto, mas não espere por ele
Reed Copsey
Eu não acho que isso seja preciso, por favor, veja as discussões na resposta de @ StephenCleary ... veja também a resposta de Servy.
Rosdi Kasim
1
se eu precisar adicionar .ConfigrtueAwait (false). Eu o adicionaria apenas a Task.WhenAll ou a cada garçom a seguir?
AstroSharp
@AstroSharp em geral, é uma boa ideia adicioná-lo a todos eles (se o primeiro for concluído, ele será efetivamente ignorado), mas neste caso, provavelmente seria bom apenas fazer o primeiro - a menos que haja mais assíncrono coisas acontecendo mais tarde.
Reed Copsey
6

Caso esteja tentando registrar todos os erros, mantenha a linha Task.WhenAll em seu código, muitos comentários sugerem que você pode removê-lo e aguardar tarefas individuais. Task.WhenAll é realmente importante para o tratamento de erros. Sem essa linha, você potencialmente deixa seu código aberto para exceções não observadas.

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Imagine o FeedCat lança uma exceção no seguinte código:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Nesse caso, você nunca esperará na houseTask nem na carTask. Existem três cenários possíveis aqui:

  1. O SellHouse já foi concluído com êxito quando o FeedCat falhou. Neste caso, você está bem.

  2. O SellHouse não está completo e falha com exceção em algum momento. A exceção não é observada e será repetida no encadeamento do finalizador.

  3. SellHouse não está completo e contém aguarda dentro dele. Caso seu código seja executado no ASP.NET SellHouse falhará assim que algumas das esperas forem concluídas dentro dele. Isso acontece porque você basicamente acionou o esquecimento de chamada e sincronização e perdeu e o contexto de sincronização foi perdido assim que o FeedCat falhou.

Aqui está o erro que você obterá no caso (3):

System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()
   --- End of inner exception stack trace ---
---> (Inner Exception #0) System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()<---

No caso (2), você receberá um erro semelhante, mas com o rastreamento da pilha de exceção original.

Para o .NET 4.0 e posterior, você pode capturar exceções não observadas usando TaskScheduler.UnobservedTaskException. Para o .NET 4.5 e posteriores, as exceções não observadas são engolidas por padrão. A exceção não observada do .NET 4.0 travará seu processo.

Mais detalhes aqui: Tratamento de exceções de tarefas no .NET 4.5

samfromlv
fonte
2

Você pode usar Task.WhenAllcomo mencionado ou Task.WaitAll, dependendo se deseja que o encadeamento aguarde. Dê uma olhada no link para obter uma explicação de ambos.

WaitAll vs WhenAll

christiandev
fonte
2

Use Task.WhenAlle aguarde os resultados:

var tCat = FeedCat();
var tHouse = SellHouse();
var tCar = BuyCar();
await Task.WhenAll(tCat, tHouse, tCar);
Cat cat = await tCat;
House house = await tHouse;
Tesla car = await tCar; 
//as they have all definitely finished, you could also use Task.Value.
It'sNotALie.
fonte
mm ... não Task.Value (talvez ele costumava existir em 2013?), em vez tCat.Result, tHouse.Result ou tCar.Result
Stephen Iorque
1

Aviso de encaminhamento

Apenas um aviso rápido para quem visita esse e outros threads semelhantes, procurando uma maneira de paralelizar o EntityFramework usando o conjunto de ferramentas assíncrona + aguardar + tarefa : o padrão mostrado aqui é bom, no entanto, quando se trata do floco de neve especial da EF, você não irá obtenha execução paralela, a menos e até que você use uma instância de contexto db (nova) separada dentro de cada chamada * Async () envolvida.

Esse tipo de coisa é necessária devido às limitações inerentes ao design dos contextos ef-db, que proíbem a execução de várias consultas em paralelo na mesma instância do contexto ef-db.


Aproveitando as respostas já fornecidas, esta é a maneira de garantir que você colete todos os valores, mesmo no caso de uma ou mais tarefas resultar em uma exceção:

  public async Task<string> Foobar() {
    async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
        return DoSomething(await a, await b, await c);
    }

    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        if (carTask.Status == TaskStatus.RanToCompletion //triple
            && catTask.Status == TaskStatus.RanToCompletion //cache
            && houseTask.Status == TaskStatus.RanToCompletion) { //hits
            return Task.FromResult(DoSomething(catTask.Result, carTask.Result, houseTask.Result)); //fast-track
        }

        cat = await catTask;
        car = await carTask;
        house = await houseTask;
        //or Task.AwaitAll(carTask, catTask, houseTask);
        //or await Task.WhenAll(carTask, catTask, houseTask);
        //it depends on how you like exception handling better

        return Awaited(catTask, carTask, houseTask);
   }
 }

Uma implementação alternativa que possui mais ou menos as mesmas características de desempenho pode ser:

 public async Task<string> Foobar() {
    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        cat = catTask.Status == TaskStatus.RanToCompletion ? catTask.Result : (await catTask);
        car = carTask.Status == TaskStatus.RanToCompletion ? carTask.Result : (await carTask);
        house = houseTask.Status == TaskStatus.RanToCompletion ? houseTask.Result : (await houseTask);

        return DoSomething(cat, car, house);
     }
 }
XDS
fonte
-1
var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());

se você deseja acessar o Cat, faça o seguinte:

var ct = (Cat)dn[0];

Isso é muito simples de fazer e muito útil de usar, não há necessidade de buscar uma solução complexa.

taurius
fonte
1
Há apenas um problema com isso: dynamicé o diabo. É para interoperabilidade COM complicada e tal, e não deve ser usado em qualquer situação em que não seja absolutamente necessário. Especialmente se você se importa com o desempenho. Ou digite segurança. Ou refatoração. Ou depuração.
Joel Mueller #