Compartilhar via


Programação assíncrona com async e await

O modelo de Programação Assíncrona de Tarefas (TAP) fornece uma camada de abstração sobre a codificação assíncrona típica. Nesse modelo, você escreve código como uma sequência de instruções, o mesmo que de costume. A diferença é que você pode ler seu código baseado em tarefa à 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 da tarefa é habilitar o código que 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 da tarefa é 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 fazer 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 fazer um café da manhã podem ser fornecidas como uma lista:

  1. Sirva uma xícara de café.
  2. Aqueça uma panela e, em seguida, frite dois ovos.
  3. Frite três fatias de bacon.
  4. Torra dois pedaços de pão.
  5. Espalhe manteiga e geléia na torrada.
  6. Despeje um copo de suco de laranja.

Se você tiver experiência com culinária, poderá concluir estas instruções de forma assíncrona. Você começa a aquecer a panela para ovos, em seguida, começar a fritar o bacon. Você coloca o pão na torradeira, depois 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 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 assistindo ativamente o processo. Assim que você começar a aquecer a panela para os ovos, você pode começar a fritar o bacon. Depois que o bacon começar a cozinhar, você pode colocar o pão na torradeira.

Para um algoritmo paralelo, você precisa de várias pessoas que cozinham (ou vários threads). Uma pessoa cozinha os ovos, outra frita o bacon, e assim por diante. Cada pessoa se concentra em uma tarefa específica. Cada pessoa que está cozinhando (ou cada thread) está bloqueada sincronizadamente enquanto espera a conclusão da tarefa atual: o bacon pronto para virar, o pão pronto para sair da 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 Bacon { }
    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");

            Bacon bacon = FryBacon(3);
            Console.WriteLine("bacon is 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 Bacon FryBacon(int slices)
        {
            Console.WriteLine($"putting {slices} slices of bacon in the pan");
            Console.WriteLine("cooking first side of bacon...");
            Task.Delay(3000).Wait();
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("flipping a slice of bacon");
            }
            Console.WriteLine("cooking the second side of bacon...");
            Task.Delay(3000).Wait();
            Console.WriteLine("Put bacon on plate");

            return new Bacon();
        }

        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 faria, o café da manhã leva cerca de 30 minutos para se preparar. A duração é a soma dos tempos de tarefa individuais. O computador bloqueia cada instrução até que todo o trabalho seja concluído e, então, prossegue para a próxima instrução de tarefa. Essa abordagem pode levar um tempo significativo. No exemplo de café da manhã, o método de computador cria um café da manhã insatisfatório. Tarefas posteriores na lista síncrona, como brindar 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 você quiser 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 ao baixar dados da Web. Ao escrever programas de servidor, você não deseja bloquear threads que possam estar atendendo a outras solicitações. O uso de código síncrono quando existem alternativas assíncronas prejudica sua capacidade de escalar de forma mais econômica. Você paga pelos threads bloqueados.

Aplicativos modernos bem-sucedidos exigem código assíncrono. Sem suporte de linguagem, escrever código assíncrono requer retornos de chamada, eventos de conclusão ou outros meios que obscureçam 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. 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 em vez disso

O código anterior realça 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 desse modelo é semelhante ao olhar para a torradeira depois que você coloca o pão. Ignore as interrupções e não inicie outras tarefas até que o pão apareça. Você não tira a manteiga e a geléia da geladeira. Você pode não perceber quando um fogo começa no fogão. Você quer torrar o pão e lidar com outras preocupações ao mesmo tempo. O mesmo acontece com 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 não bloqueante de iniciar uma tarefa e continuar a execução quando a tarefa for concluída. Uma versão simples e assíncrona do código de 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");

    Bacon bacon = await FryBaconAsync(3);
    Console.WriteLine("bacon is 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 de FryEggsAsync, FryBaconAsynce ToastBreadAsync para retornar objetos Task<Egg>, Task<Bacon>e Task<Toast>, respectivamente. Os nomes de método incluem o sufixo "Async". O método Main retorna o objeto Task, embora não tenha uma expressão return, que é por design. Para obter mais informações, consulte Avaliação de uma função assíncrona que retorna nulo.

Nota

O código atualizado ainda não aproveita os principais recursos de programação assíncrona, o que pode resultar em tempos de conclusão mais curtos. O código processa as tarefas na mesma quantidade 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 posteriormente neste artigo.

Vamos aplicar o exemplo de café da manhã ao código atualizado. O thread não bloqueia enquanto os ovos ou o bacon estão sendo cozidos, mas o código também não inicia outras tarefas até que o trabalho atual seja concluído. Você ainda coloca o pão na torradeira e fica olhando para a torradeira até que o pão salte para fora, 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 está fazendo o café da manhã não é bloqueado enquanto aguarda qualquer tarefa iniciada que esteja inacabada. Para alguns aplicativos, essa alteração é tudo o que você precisa. Você pode habilitar seu aplicativo para dar 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 for concluída, você iniciará outro trabalho pronto para começar. Ao aplicar essa metodologia ao exemplo de café da manhã, você pode preparar o café da manhã mais rapidamente. Você também prepara tudo quase ao mesmo tempo, para que 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 se assemelha mais à maneira como você cria o café da manhã na vida real. Você começa a cozinhar os ovos, bacon e torradas ao mesmo tempo. Como cada item alimentar requer ação, você volta sua atenção para essa tarefa, conclui a ação e, em seguida, espera por outra coisa que exija sua atenção.

Em seu código, você inicia uma tarefa e se mantém no objeto Task que representa o trabalho. Use o método await na tarefa para atrasar a atuação no trabalho até que o resultado esteja pronto.

Aplique essas alterações ao código do café da manhã. 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<Bacon> baconTask = FryBaconAsync(3);
Bacon bacon = await baconTask;
Console.WriteLine("Bacon is 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 seu café da manhã mais rápido. A expressão await é aplicada a todas as tarefas assim que elas começam. A próxima etapa é mover as expressões de await do bacon e dos 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<Bacon> baconTask = FryBaconAsync(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");
Bacon bacon = await baconTask;
Console.WriteLine("Bacon is ready");

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

Agora você tem um café da manhã preparado antecipadamente, que leva cerca de 20 minutos para ficar pronto. O tempo total de cozimento é reduzido porque algumas tarefas são executadas simultaneamente.

Diagrama que mostra instruções para preparar o café da manhã como oito tarefas assíncronas que são concluídas em cerca de 20 minutos, onde infelizmente, os ovos e bacon 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 bacon. Você inicia todas as tarefas assíncronas de uma só vez. Você aguarda cada tarefa somente quando precisar dos resultados. O código pode ser semelhante ao programa em um aplicativo Web que faz solicitações para microsserviços diferentes e, em seguida, combina os resultados em uma única página. Faça todas as solicitações imediatamente, aplique a expressão await em todas essas tarefas e componha a página web.

Suportar a composição com tarefas

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

Importante

A composição de uma operação assíncrona seguida pelo trabalho síncrono é uma operação assíncrona. 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 objetos Task ou Task<TResult> para realizar tarefas em execução. Aguarde a conclusão de cada tarefa antes de utilizar o resultado. A próxima etapa é criar métodos que representem a combinação de outros trabalhos. Antes de servir o café da manhã, você deve esperar pela tarefa de tostar o pão antes de passar a manteiga e a geleia.

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 para o compilador que o método contém uma expressão await e contém operações assíncronas. Este método representa a tarefa que torra o pão e, em seguida, passa manteiga e geleia. O método retorna um objeto Task<TResult> que representa a composição das três operações.

O bloco principal de código 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 baconTask = FryBaconAsync(3);
    var toastTask = MakeToastWithButterAndJamAsync(2);

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

    var bacon = await baconTask;
    Console.WriteLine("bacon is 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 por essa tarefa. Você pode iniciar outras tarefas simultaneamente.

Manipular exceções assíncronas

Até esse ponto, seu código pressupõe implicitamente que todas as tarefas sejam concluídas com êxito. Métodos assíncronos geram exceções, assim como seus equivalentes síncronos. As metas de suporte assíncrono para exceções e tratamento de erros são as mesmas do suporte assíncrono em geral. A melhor prática é escrever um código que seja lido como uma série de instruções síncronas. As tarefas geram exceções quando não podem ser concluídas com êxito. 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 pegue fogo enquanto torra o pão. Você pode simular esse problema modificando o método ToastBreadAsync para corresponder ao seguinte código:

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();
}

Nota

Ao compilar esse código, você verá um aviso sobre código inacessível. Esse erro é por design. Depois que a torradeira pega fogo, as operações não prossseguem 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 slices of bacon in the pan
Cooking first side of bacon...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
Fire! Toast is ruined!
Flipping a slice of bacon
Flipping a slice of bacon
Flipping a slice of bacon
Cooking the second side of bacon...
Cracking 2 eggs
Cooking the eggs ...
Put bacon on plate
Put eggs on plate
Eggs are ready
Bacon is 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 algumas tarefas são concluídas entre o momento em que a torradeira pega fogo e o sistema observa a exceção. Quando uma tarefa que é executada de forma assíncrona gera uma exceção, ocorre uma falha nessa tarefa. O objeto Task contém a exceção gerada na propriedade Task.Exception. Tarefas com falha geram uma exceção quando a expressão await é aplicada à tarefa.

Há dois mecanismos importantes para entender sobre esse processo:

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

Quando o código em execução de forma assíncrona gera uma exceção, a exceção é armazenada no objeto Task. A propriedade Task.Exception é um objeto System.AggregateException, pois mais de uma exceção pode ser gerada durante o trabalho assíncrono. Qualquer exceção gerada é adicionada à coleção AggregateException.InnerExceptions. Se a propriedade Exception for nula, um novo objeto AggregateException será criado e a exceção gerada será o primeiro item da 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 na coleção. Esse resultado é o motivo pelo 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 ser o mais semelhante possível ao trabalho com seus equivalentes síncronos. 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 de métodos de retorno de tarefa. Para obter mais informações e exemplos, consulte Exceções em métodos de retorno de tarefa.

Antes de continuar para a próxima seção, comente as duas instruções a seguir em 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 await às tarefas com eficiência

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 este método:

await Task.WhenAll(eggsTask, baconTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Bacon is 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 primeira tarefa para concluir e, em seguida, processar seu resultado. Depois de processar o resultado da tarefa concluída, você remove a tarefa concluída da lista de tarefas passadas para o método WhenAny.

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

Próximo ao final do snippet de código, observe a expressão await finishedTask;. A expressão await Task.WhenAny não aguarda a tarefa concluída, mas aguarda o objeto Task retornado pelo método Task.WhenAny. O resultado do método Task.WhenAny é a tarefa concluída (ou com falha). A melhor prática é aguardar a tarefa novamente, mesmo quando você souber que a tarefa está concluída. Dessa maneira, você pode recuperar o resultado da tarefa ou garantir que qualquer exceção que faça a tarefa falhar seja gerada.

Examinar o código final

Veja a aparência da 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 Bacon { }
    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 baconTask = FryBaconAsync(3);
            var toastTask = MakeToastWithButterAndJamAsync(2);

            var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
            while (breakfastTasks.Count > 0)
            {
                Task finishedTask = await Task.WhenAny(breakfastTasks);
                if (finishedTask == eggsTask)
                {
                    Console.WriteLine("eggs are ready");
                }
                else if (finishedTask == baconTask)
                {
                    Console.WriteLine("bacon is 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<Bacon> FryBaconAsync(int slices)
        {
            Console.WriteLine($"putting {slices} slices of bacon in the pan");
            Console.WriteLine("cooking first side of bacon...");
            await Task.Delay(3000);
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("flipping a slice of bacon");
            }
            Console.WriteLine("cooking the second side of bacon...");
            await Task.Delay(3000);
            Console.WriteLine("Put bacon on plate");

            return new Bacon();
        }

        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 ação 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. 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 ações principais ainda estão claras lendo o código. Você pode ler o código final da mesma forma que leu a lista de instruções para fazer um café da manhã, conforme 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 para seguir as instruções escritas: Inicie as tarefas como puder e não bloqueie enquanto aguarda a conclusão das tarefas.

Próxima etapa