Partilhar via


Programação assíncrona com async e aguarde

O modelo de programação assíncrona de tarefas (TAP) fornece uma camada de abstração sobre a codificação assíncrona típica. Neste modelo, você escreve código como uma sequência de instruções, o mesmo de sempre. A diferença é que você pode ler seu código baseado em tarefas à medida que o compilador processa cada instrução e antes de começar a processar a próxima instrução. Para realizar esse modelo, o compilador executa muitas transformações para concluir cada tarefa. Algumas instruções podem iniciar o trabalho e retornar um objeto Task que representa o trabalho em andamento e o compilador deve resolver essas transformações. O objetivo da programação assíncrona de tarefas é habilitar o código que se lê como uma sequência de instruções, mas é executado em uma ordem mais complicada. A execução é baseada na alocação de recursos externos e quando as tarefas são concluídas.

O modelo de programação assíncrona de tarefas é análogo à forma como as pessoas dão instruções para processos que incluem tarefas assíncronas. Este artigo usa um exemplo com instruções para preparar o café da manhã para mostrar como as palavras-chave async e await facilitam o raciocínio sobre o código que inclui uma série de instruções assíncronas. As instruções para preparar um pequeno-almoço podem ser fornecidas como uma lista:

  1. Despeje uma xícara de café.
  2. Aqueça uma panela e, em seguida, frite dois ovos.
  3. Cozinhe três rissóis castanhos de haxixe.
  4. Tostar dois pedaços de pão.
  5. Espalhe manteiga e geléia na torrada.
  6. Deite um copo de sumo de laranja.

Se tiver experiência com cozinhar, pode completar estas instruções de forma assíncrona. Você começa a aquecer a panela para ovos, em seguida, começa a cozinhar os hash browns. Você coloca o pão na torradeira e, em seguida, começa a cozinhar os ovos. Em cada etapa do processo, você inicia uma tarefa e, em seguida, faz a transição para outras tarefas que estão prontas para sua atenção.

Cozinhar o café da manhã é um bom exemplo de trabalho assíncrono que não é paralelo. Uma pessoa (ou thread) pode lidar com todas as tarefas. Uma pessoa pode fazer o café da manhã de forma assíncrona, iniciando a próxima tarefa antes que a tarefa anterior seja concluída. Cada tarefa de cozimento progride, independentemente de alguém estar ativamente observando o processo. Assim que você começar a aquecer a panela para os ovos, você pode começar a cozinhar os hash browns. Depois que os hash browns começarem a cozinhar, você pode colocar o pão na torradeira.

Para um algoritmo paralelo, precisas de várias pessoas que cozinham (ou várias threads). Uma pessoa cozinha os ovos, outra cozinha os hash browns, e assim por diante. Cada pessoa se concentra em sua tarefa específica. Cada pessoa que está cozinhando (ou cada fio) é bloqueada de forma síncrona aguardando a conclusão da tarefa atual: Hash browns prontos para virar, pão pronto para aparecer na torradeira e assim por diante.

Diagrama que mostra instruções para preparar o café da manhã como uma lista de sete tarefas sequenciais concluídas em 30 minutos.

Considere a mesma lista de instruções síncronas escritas como instruções de código C#:

using System;
using System.Threading.Tasks;

namespace AsyncBreakfast
{
    // These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
    internal class HashBrown { }
    internal class Coffee { }
    internal class Egg { }
    internal class Juice { }
    internal class Toast { }

    class Program
    {
        static void Main(string[] args)
        {
            Coffee cup = PourCoffee();
            Console.WriteLine("coffee is ready");

            Egg eggs = FryEggs(2);
            Console.WriteLine("eggs are ready");

            HashBrown hashBrown = FryHashBrowns(3);
            Console.WriteLine("hash browns are ready");

            Toast toast = ToastBread(2);
            ApplyButter(toast);
            ApplyJam(toast);
            Console.WriteLine("toast is ready");

            Juice oj = PourOJ();
            Console.WriteLine("oj is ready");
            Console.WriteLine("Breakfast is ready!");
        }

        private static Juice PourOJ()
        {
            Console.WriteLine("Pouring orange juice");
            return new Juice();
        }

        private static void ApplyJam(Toast toast) =>
            Console.WriteLine("Putting jam on the toast");

        private static void ApplyButter(Toast toast) =>
            Console.WriteLine("Putting butter on the toast");

        private static Toast ToastBread(int slices)
        {
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("Putting a slice of bread in the toaster");
            }
            Console.WriteLine("Start toasting...");
            Task.Delay(3000).Wait();
            Console.WriteLine("Remove toast from toaster");

            return new Toast();
        }

        private static HashBrown FryHashBrowns(int patties)
        {
            Console.WriteLine($"putting {patties} hash brown patties in the pan");
            Console.WriteLine("cooking first side of hash browns...");
            Task.Delay(3000).Wait();
            for (int patty = 0; patty < patties; patty++)
            {
                Console.WriteLine("flipping a hash brown patty");
            }
            Console.WriteLine("cooking the second side of hash browns...");
            Task.Delay(3000).Wait();
            Console.WriteLine("Put hash browns on plate");

            return new HashBrown();
        }

        private static Egg FryEggs(int howMany)
        {
            Console.WriteLine("Warming the egg pan...");
            Task.Delay(3000).Wait();
            Console.WriteLine($"cracking {howMany} eggs");
            Console.WriteLine("cooking the eggs ...");
            Task.Delay(3000).Wait();
            Console.WriteLine("Put eggs on plate");

            return new Egg();
        }

        private static Coffee PourCoffee()
        {
            Console.WriteLine("Pouring coffee");
            return new Coffee();
        }
    }
}

Se você interpretar essas instruções como um computador, o café da manhã leva cerca de 30 minutos para ser preparado. A duração é a soma dos tempos de cada tarefa. O computador bloqueia cada instrução até que todo o trabalho seja concluído e, em seguida, prossegue para a próxima instrução de tarefa. Esta abordagem pode levar um tempo significativo. No exemplo de café da manhã, o método de computador cria um café da manhã insatisfatório. As tarefas posteriores na lista síncrona, como torrar o pão, não são iniciadas até que as tarefas anteriores sejam concluídas. Alguns alimentos ficam frios antes do café da manhã estar pronto para servir.

Se desejar que o computador execute instruções de forma assíncrona, você deve escrever código assíncrono. Ao escrever programas cliente, você deseja que a interface do usuário responda à entrada do usuário. Seu aplicativo não deve congelar toda a interação durante o download de dados da Web. Quando você escreve programas de servidor, você não deseja bloquear threads que podem estar atendendo a outras solicitações. Usar código síncrono quando existem alternativas assíncronas prejudica sua capacidade de expandir de forma menos dispendiosa. Você paga por threads bloqueados.

Aplicativos modernos bem-sucedidos exigem código assíncrono. Sem suporte a idiomas, escrever código assíncrono requer retornos de chamada, eventos de conclusão ou outros meios que obscurecem a intenção original do código. A vantagem do código síncrono é a ação passo a passo que facilita a verificação e a compreensão. Os modelos assíncronos tradicionais forçam você a se concentrar na natureza assíncrona do código, não nas ações fundamentais do código.

Não bloqueie, aguarde

O código anterior destaca uma prática de programação infeliz: escrever código síncrono para executar operações assíncronas. O código impede que o thread atual faça qualquer outro trabalho. O código não interrompe o thread enquanto há tarefas em execução. O resultado deste modelo é semelhante a olhar para a torradeira depois de colocar o pão. Você ignora quaisquer interrupções e não inicia outras tarefas até que o pão apareça. Você não tira a manteiga e geléia da geladeira. Você pode não perceber que um incêndio está começando no fogão. Você quer torrar o pão e lidar com outras preocupações ao mesmo tempo. O mesmo acontece com o seu código.

Você pode começar atualizando o código para que o thread não seja bloqueado enquanto as tarefas estão em execução. A palavra-chave await fornece uma maneira sem bloqueio de iniciar uma tarefa e, em seguida, continuar a execução quando a tarefa for concluída. Uma versão assíncrona simples do código do café da manhã se parece com o seguinte trecho:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");

    Egg eggs = await FryEggsAsync(2);
    Console.WriteLine("eggs are ready");

    HashBrown hashBrown = await FryHashBrownsAsync(3);
    Console.WriteLine("hash browns are ready");

    Toast toast = await ToastBreadAsync(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("toast is ready");

    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");
    Console.WriteLine("Breakfast is ready!");
}

O código atualiza os corpos do método original de FryEggs, FryHashBrownse ToastBread para retornar Task<Egg>, Task<HashBrown>, e Task<Toast> objetos, respectivamente. Os nomes de método atualizados incluem o sufixo "Async": FryEggsAsync, FryHashBrownsAsynce ToastBreadAsync. O método Main retorna o objeto Task, embora ele não tenha uma expressão return, que é por design. Para obter mais informações, consulte Avaliação de uma função assíncrona de retorno de vazio.

Observação

O código atualizado ainda não aproveita os principais recursos da programação assíncrona, o que pode resultar em tempos de conclusão mais curtos. O código processa as tarefas aproximadamente no mesmo período de tempo que a versão síncrona inicial. Para obter as implementações completas do método, consulte a versão final do código mais adiante neste artigo.

Vamos aplicar o exemplo de café da manhã ao código atualizado. A thread não bloqueia enquanto os ovos ou as batatas raladas estão a cozinhar, mas o código tampouco inicia outras tarefas até que o trabalho atual seja concluído. Você ainda coloca o pão na torradeira e olha para a torradeira até que o pão apareça, mas agora você pode responder às interrupções. Em um restaurante onde vários pedidos são feitos, o cozinheiro pode começar um novo pedido enquanto outro já está cozinhando.

No código atualizado, o thread que trabalha no café da manhã não é bloqueado enquanto aguarda qualquer tarefa iniciada que esteja inacabada. Para algumas aplicações, esta alteração é tudo o que precisa. Você pode habilitar seu aplicativo para oferecer suporte à interação do usuário enquanto os dados são baixados da Web. Em outros cenários, talvez você queira iniciar outras tarefas enquanto aguarda a conclusão da tarefa anterior.

Iniciar tarefas simultaneamente

Para a maioria das operações, você deseja iniciar várias tarefas independentes imediatamente. À medida que cada tarefa é concluída, você inicia outro trabalho que está pronto para começar. Quando você aplica essa metodologia ao exemplo do café da manhã, você pode preparar o café da manhã mais rapidamente. Você também deixa tudo pronto quase ao mesmo tempo, para que você possa desfrutar de um café da manhã quente.

A classe System.Threading.Tasks.Task e os tipos relacionados são classes que você pode usar para aplicar esse estilo de raciocínio a tarefas que estão em andamento. Essa abordagem permite que você escreva um código que mais se assemelha à maneira como você cria o café da manhã na vida real. Você começa a cozinhar os ovos, hash browns e torradas ao mesmo tempo. Como cada alimento requer ação, você volta a sua atenção para essa tarefa, realiza a ação e, em seguida, espera por outra coisa que exija a sua atenção.

No seu código, você inicia uma tarefa e mantém o objeto Task que representa o trabalho. Use o método await na tarefa para adiar a execução do trabalho até que o resultado esteja pronto.

Aplique estas alterações ao código do pequeno-almoço. A primeira etapa é armazenar as tarefas para operações quando elas forem iniciadas, em vez de usar a expressão await:

Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");

Task<Egg> eggsTask = FryEggsAsync(2);
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");

Task<HashBrown> hashBrownTask = FryHashBrownsAsync(3);
HashBrown hashBrown = await hashBrownTask;
Console.WriteLine("Hash browns are ready");

Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");

Juice oj = PourOJ();
Console.WriteLine("Oj is ready");
Console.WriteLine("Breakfast is ready!");

Essas revisões não ajudam a preparar o café da manhã mais rápido. A expressão await é aplicada a todas as tarefas assim que elas são iniciadas. O próximo passo é mover as await expressões para os hash browns e ovos para o final do método, antes de servir o café da manhã:

Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");

Task<Egg> eggsTask = FryEggsAsync(2);
Task<HashBrown> hashBrownTask = FryHashBrownsAsync(3);
Task<Toast> toastTask = ToastBreadAsync(2);

Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");

Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
HashBrown hashBrown = await hashBrownTask;
Console.WriteLine("Hash browns are ready");

Console.WriteLine("Breakfast is ready!");

Agora você tem um café da manhã preparado de forma assíncrona que leva cerca de 20 minutos para ser preparado. O tempo total de cozedura é reduzido porque algumas tarefas são executadas em simultâneo.

Diagrama que mostra instruções para preparar o café da manhã como oito tarefas assíncronas que se concluem em cerca de 20 minutos, onde, infelizmente, os ovos e hash browns queimam.

As atualizações de código melhoram o processo de preparação, reduzindo o tempo de cozimento, mas introduzem uma regressão queimando os ovos e os hash browns. Você inicia todas as tarefas assíncronas de uma só vez. Aguarde em cada tarefa apenas quando precisar dos resultados. O código pode ser semelhante ao programa em um aplicativo Web que faz solicitações para diferentes microsserviços e, em seguida, combina os resultados em uma única página. Você faz todas as solicitações imediatamente e, em seguida, aplica a expressão await em todas essas tarefas e compõe a página da Web.

Composição de suporte com tarefas

As revisões de código anteriores ajudam a preparar tudo para o pequeno-almoço ao mesmo tempo, exceto a torrada. O processo de confeção da torrada é uma composição de uma operação assíncrona (torrar o pão) com operações síncronas (espalhar manteiga e geléia na torrada). Este exemplo ilustra um conceito importante sobre programação assíncrona:

Importante

A composição de uma operação assíncrona seguida de trabalho síncrono é uma operação assíncrona. Dito de outra forma, se qualquer parte de uma operação for assíncrona, toda a operação será assíncrona.

Nas atualizações anteriores, você aprendeu a usar Task ou Task<TResult> objetos para manter tarefas em execução. Você aguarda cada tarefa antes de usar seu resultado. O próximo passo é criar métodos que representem a combinação de outros trabalhos. Antes de servir o pequeno-almoço, deve esperar pela tarefa que representa torrar o pão antes de espalhar a manteiga e a compota.

Você pode representar esse trabalho com o seguinte código:

static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
    var toast = await ToastBreadAsync(number);
    ApplyButter(toast);
    ApplyJam(toast);

    return toast;
}

O método MakeToastWithButterAndJamAsync tem o modificador async em sua assinatura que sinaliza ao compilador que o método contém uma expressão await e contém operações assíncronas. O método representa a tarefa de torrar o pão e depois espalhar a manteiga e a geleia. O método retorna um objeto Task<TResult> que representa a composição das três operações.

O bloco de código principal revisado agora tem esta aparência:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");

    var eggsTask = FryEggsAsync(2);
    var hashBrownTask = FryHashBrownsAsync(3);
    var toastTask = MakeToastWithButterAndJamAsync(2);

    var eggs = await eggsTask;
    Console.WriteLine("eggs are ready");

    var hashBrown = await hashBrownTask;
    Console.WriteLine("hash browns are ready");

    var toast = await toastTask;
    Console.WriteLine("toast is ready");

    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");
    Console.WriteLine("Breakfast is ready!");
}

Essa alteração de código ilustra uma técnica importante para trabalhar com código assíncrono. Você compõe tarefas separando as operações em um novo método que retorna uma tarefa. Você pode escolher quando esperar nessa tarefa. Você pode iniciar outras tarefas simultaneamente.

Manipular exceções assíncronas

Até este ponto, seu código assume implicitamente todas as tarefas concluídas com êxito. Os métodos assíncronos lançam exceções, assim como suas contrapartes síncronas. As metas para suporte assíncrono para exceções e tratamento de erros são as mesmas que para suporte assíncrono em geral. A prática recomendada é escrever código que se lê como uma série de instruções síncronas. As tarefas lançam exceções quando não podem ser concluídas com sucesso. O código do cliente pode capturar essas exceções quando a expressão await é aplicada a uma tarefa iniciada.

No exemplo do café da manhã, suponha que a torradeira pega fogo enquanto torra o pão. Você pode simular esse problema modificando o método ToastBreadAsync para corresponder ao código a seguir:

private static async Task<Toast> ToastBreadAsync(int slices)
{
    for (int slice = 0; slice < slices; slice++)
    {
        Console.WriteLine("Putting a slice of bread in the toaster");
    }
    Console.WriteLine("Start toasting...");
    await Task.Delay(2000);
    Console.WriteLine("Fire! Toast is ruined!");
    throw new InvalidOperationException("The toaster is on fire");
    await Task.Delay(1000);
    Console.WriteLine("Remove toast from toaster");

    return new Toast();
}

Observação

Quando você compila esse código, você vê um aviso sobre código inacessível. Este erro é por design. Depois que a torradeira pega fogo, as operações não prosseguem normalmente e o código retorna um erro.

Depois de fazer as alterações de código, execute o aplicativo e verifique a saída:

Pouring coffee
Coffee is ready
Warming the egg pan...
putting 3 hash brown patties in the pan
Cooking first side of hash browns...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
Fire! Toast is ruined!
Flipping a hash brown patty
Flipping a hash brown patty
Flipping a hash brown patty
Cooking the second side of hash browns...
Cracking 2 eggs
Cooking the eggs ...
Put hash browns on plate
Put eggs on plate
Eggs are ready
Hash browns are ready
Unhandled exception. System.InvalidOperationException: The toaster is on fire
   at AsyncBreakfast.Program.ToastBreadAsync(Int32 slices) in Program.cs:line 65
   at AsyncBreakfast.Program.MakeToastWithButterAndJamAsync(Int32 number) in Program.cs:line 36
   at AsyncBreakfast.Program.Main(String[] args) in Program.cs:line 24
   at AsyncBreakfast.Program.<Main>(String[] args)

Observe que muitas tarefas terminam entre o momento em que a torradeira pega fogo e o sistema observa a exceção. Quando uma tarefa executada de forma assíncrona lança uma exceção, essa tarefa apresenta falhas. O objeto Task contém a exceção gerada na propriedade Task.Exception. As tarefas com falha geram uma exceção quando a expressão await é aplicada à tarefa.

Existem dois mecanismos importantes para entender sobre esse processo:

  • Como uma exceção é armazenada em uma tarefa com defeito
  • Como uma exceção é desembalada e relançada quando o código aguarda (await) em uma tarefa com falha

Quando o código em execução assíncrona lança uma exceção, a exceção é armazenada no objeto Task. A propriedade Task.Exception é um objeto System.AggregateException porque podem ocorrer várias exceções durante o processamento assíncrono. Qualquer exceção lançada é adicionada à coleção AggregateException.InnerExceptions. Se a propriedade Exception for nula, cria-se um novo objeto AggregateException e a exceção lançada será o primeiro item dessa coleção.

O cenário mais comum para uma tarefa com falha é que a propriedade Exception contém exatamente uma exceção. Quando o seu código espera por uma tarefa com falha, ele relança a primeira exceção AggregateException.InnerExceptions da coleção. Esse resultado é a razão pela qual a saída do exemplo mostra um objeto System.InvalidOperationException em vez de um objeto AggregateException. Extrair a primeira exceção interna torna o trabalho com métodos assíncronos o mais semelhante possível ao trabalho com suas contrapartes síncronas. Você pode examinar a propriedade Exception em seu código quando seu cenário pode gerar várias exceções.

Dica

A prática recomendada é que quaisquer exceções de validação de argumento surjam de forma síncrona a partir de métodos de retorno de tarefas. Para obter mais informações e exemplos, consulte Exceções em métodos de retorno de tarefas.

Antes de avançar para a próxima secção, comente as seguintes duas instruções no seu método ToastBreadAsync. Você não quer iniciar outro incêndio:

Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");

Aplique expressões de espera a tarefas de forma eficiente

Você pode melhorar a série de expressões await no final do código anterior usando métodos da classe Task. Uma API é o método WhenAll, que retorna um objeto Task que é concluído quando todas as tarefas em sua lista de argumentos são concluídas. O código a seguir demonstra esse método:

await Task.WhenAll(eggsTask, hashBrownTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Hash browns are ready");
Console.WriteLine("Toast is ready");
Console.WriteLine("Breakfast is ready!");

Outra opção é usar o método WhenAny, que retorna um objeto Task<Task> que é concluído quando qualquer um de seus argumentos é concluído. Você pode aguardar a tarefa retornada porque sabe que a tarefa foi concluída. O código a seguir mostra como você pode usar o método WhenAny para aguardar a conclusão da primeira tarefa e, em seguida, processar seu resultado. Depois de processar o resultado da tarefa concluída, remova a tarefa concluída da lista de tarefas passadas para o método WhenAny.

var breakfastTasks = new List<Task> { eggsTask, hashBrownTask, toastTask };
while (breakfastTasks.Count > 0)
{
    Task finishedTask = await Task.WhenAny(breakfastTasks);
    if (finishedTask == eggsTask)
    {
        Console.WriteLine("Eggs are ready");
    }
    else if (finishedTask == hashBrownTask)
    {
        Console.WriteLine("Hash browns are ready");
    }
    else if (finishedTask == toastTask)
    {
        Console.WriteLine("Toast is ready");
    }
    await finishedTask;
    breakfastTasks.Remove(finishedTask);
}

Perto do final do trecho de código, observe a expressão await finishedTask;. Essa linha é importante porque Task.WhenAny retorna uma Task<Task> - uma tarefa de wrapper que contém a tarefa concluída. Quando await Task.WhenAny, está a aguardar que a tarefa de encapsulamento seja concluída, e o resultado é a tarefa real que terminou primeiro. No entanto, para recuperar o resultado dessa tarefa ou garantir que quaisquer exceções sejam lançadas corretamente, você deve await executar a própria tarefa concluída (armazenada em finishedTask). Mesmo que você saiba que a tarefa terminou, aguardar novamente permite que você acesse seu resultado ou manipule quaisquer exceções que possam ter causado falhas.

Rever o código final

Veja como é a versão final do código:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace AsyncBreakfast
{
    // These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
    internal class HashBrown { }
    internal class Coffee { }
    internal class Egg { }
    internal class Juice { }
    internal class Toast { }

    class Program
    {
        static async Task Main(string[] args)
        {
            Coffee cup = PourCoffee();
            Console.WriteLine("coffee is ready");

            var eggsTask = FryEggsAsync(2);
            var hashBrownTask = FryHashBrownsAsync(3);
            var toastTask = MakeToastWithButterAndJamAsync(2);

            var breakfastTasks = new List<Task> { eggsTask, hashBrownTask, toastTask };
            while (breakfastTasks.Count > 0)
            {
                Task finishedTask = await Task.WhenAny(breakfastTasks);
                if (finishedTask == eggsTask)
                {
                    Console.WriteLine("eggs are ready");
                }
                else if (finishedTask == hashBrownTask)
                {
                    Console.WriteLine("hash browns are ready");
                }
                else if (finishedTask == toastTask)
                {
                    Console.WriteLine("toast is ready");
                }
                await finishedTask;
                breakfastTasks.Remove(finishedTask);
            }

            Juice oj = PourOJ();
            Console.WriteLine("oj is ready");
            Console.WriteLine("Breakfast is ready!");
        }

        static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
        {
            var toast = await ToastBreadAsync(number);
            ApplyButter(toast);
            ApplyJam(toast);

            return toast;
        }

        private static Juice PourOJ()
        {
            Console.WriteLine("Pouring orange juice");
            return new Juice();
        }

        private static void ApplyJam(Toast toast) =>
            Console.WriteLine("Putting jam on the toast");

        private static void ApplyButter(Toast toast) =>
            Console.WriteLine("Putting butter on the toast");

        private static async Task<Toast> ToastBreadAsync(int slices)
        {
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("Putting a slice of bread in the toaster");
            }
            Console.WriteLine("Start toasting...");
            await Task.Delay(3000);
            Console.WriteLine("Remove toast from toaster");

            return new Toast();
        }

        private static async Task<HashBrown> FryHashBrownsAsync(int patties)
        {
            Console.WriteLine($"putting {patties} hash brown patties in the pan");
            Console.WriteLine("cooking first side of hash browns...");
            await Task.Delay(3000);
            for (int patty = 0; patty < patties; patty++)
            {
                Console.WriteLine("flipping a hash brown patty");
            }
            Console.WriteLine("cooking the second side of hash browns...");
            await Task.Delay(3000);
            Console.WriteLine("Put hash browns on plate");

            return new HashBrown();
        }

        private static async Task<Egg> FryEggsAsync(int howMany)
        {
            Console.WriteLine("Warming the egg pan...");
            await Task.Delay(3000);
            Console.WriteLine($"cracking {howMany} eggs");
            Console.WriteLine("cooking the eggs ...");
            await Task.Delay(3000);
            Console.WriteLine("Put eggs on plate");

            return new Egg();
        }

        private static Coffee PourCoffee()
        {
            Console.WriteLine("Pouring coffee");
            return new Coffee();
        }
    }
}

O código conclui as tarefas assíncronas do café da manhã em cerca de 15 minutos. O tempo total é reduzido porque algumas tarefas são executadas simultaneamente. O código monitora simultaneamente várias tarefas e executa ações somente conforme necessário.

Diagrama que mostra instruções para preparar o café da manhã como seis tarefas assíncronas que são concluídas em cerca de 15 minutos, e o código monitora possíveis interrupções.

O código final é assíncrono. Ele reflete com mais precisão como uma pessoa pode cozinhar o café da manhã. Compare o código final com o primeiro exemplo de código no artigo. As principais ações ainda são claras através da leitura do código. Você pode ler o código final da mesma forma que lê a lista de instruções para fazer um café da manhã, como mostrado no início do artigo. Os recursos de idioma para as palavras-chave async e await fornecem a tradução que cada pessoa faz das instruções escritas: Comece as tarefas quando puder e não bloqueie enquanto espera que sejam concluídas as tarefas.

Async/await vs ContinueWith

As palavras-chave async e await fornecem simplificação sintática sobre o uso direto de Task.ContinueWith. Embora async/await e ContinueWith tenham semântica semelhante para lidar com operações assíncronas, o compilador não necessariamente traduz expressões await diretamente em chamadas de método ContinueWith. Em vez disso, o compilador gera código de máquina de estado otimizado que fornece o mesmo comportamento lógico. Essa transformação oferece benefícios significativos de legibilidade e manutenção, especialmente ao encadear várias operações assíncronas.

Considere um cenário em que você precise executar várias operações assíncronas sequenciais. Veja como a mesma lógica parece quando implementada com ContinueWith comparado a async/await:

Usando ContinueWith

Com ContinueWith, cada etapa numa sequência de operações assíncronas requer continuations aninhadas.

// Using ContinueWith - demonstrates the complexity when chaining operations
static Task MakeBreakfastWithContinueWith()
{
    return StartCookingEggsAsync()
        .ContinueWith(eggsTask =>
        {
            var eggs = eggsTask.Result;
            Console.WriteLine("Eggs ready, starting bacon...");
            return StartCookingBaconAsync();
        })
        .Unwrap()
        .ContinueWith(baconTask =>
        {
            var bacon = baconTask.Result;
            Console.WriteLine("Bacon ready, starting toast...");
            return StartToastingBreadAsync();
        })
        .Unwrap()
        .ContinueWith(toastTask =>
        {
            var toast = toastTask.Result;
            Console.WriteLine("Toast ready, applying butter...");
            return ApplyButterAsync(toast);
        })
        .Unwrap()
        .ContinueWith(butteredToastTask =>
        {
            var butteredToast = butteredToastTask.Result;
            Console.WriteLine("Butter applied, applying jam...");
            return ApplyJamAsync(butteredToast);
        })
        .Unwrap()
        .ContinueWith(finalToastTask =>
        {
            var finalToast = finalToastTask.Result;
            Console.WriteLine("Breakfast completed with ContinueWith!");
        });
}

Utilização de async/await

A mesma sequência de operações usando async/await lê-se muito mais naturalmente:

// Using async/await - much cleaner and easier to read
static async Task MakeBreakfastWithAsyncAwait()
{
    var eggs = await StartCookingEggsAsync();
    Console.WriteLine("Eggs ready, starting bacon...");
    
    var bacon = await StartCookingBaconAsync();
    Console.WriteLine("Bacon ready, starting toast...");
    
    var toast = await StartToastingBreadAsync();
    Console.WriteLine("Toast ready, applying butter...");
    
    var butteredToast = await ApplyButterAsync(toast);
    Console.WriteLine("Butter applied, applying jam...");
    
    var finalToast = await ApplyJamAsync(butteredToast);
    Console.WriteLine("Breakfast completed with async/await!");
}

Por que async/await é preferível

A async/await abordagem oferece várias vantagens:

  • Legibilidade: O código é lido como código síncrono, facilitando a compreensão do fluxo de operações.
  • Capacidade de manutenção: Adicionar ou remover etapas na sequência requer alterações mínimas no código.
  • Tratamento de erros: O tratamento de exceções com try/catch blocos funciona naturalmente, enquanto ContinueWith requer um tratamento cuidadoso de tarefas com falhas.
  • Depuração: A experiência com a pilha de chamadas e o depurador é muito melhor com async/await.
  • Desempenho: As otimizações do compilador são async/await mais sofisticadas do que as cadeias manuais.ContinueWith

O benefício torna-se ainda mais evidente à medida que o número de operações encadeadas aumenta. Embora uma única continuação possa ser gerenciável com ContinueWith, sequências de 3-4 ou mais operações assíncronas rapidamente se tornam difíceis de ler e manter. Esse padrão, conhecido como "notação monádica" na programação funcional, permite compor várias operações assíncronas de forma sequencial e legível.

Próximo passo