Partilhar via


Cenários de programação assíncrona

Se você tiver alguma necessidade vinculada a E/S (como solicitar dados de uma rede, acessar um banco de dados ou ler e gravar em um sistema de arquivos), convém utilizar a programação assíncrona. Você também pode ter código vinculado à CPU, como executar um cálculo caro, que também é um bom cenário para escrever código assíncrono.

O C# tem um modelo de programação assíncrona em nível de linguagem, que permite escrever facilmente código assíncrono sem ter que fazer malabarismos com retornos de chamada ou estar em conformidade com uma biblioteca que suporta assincronia. Ele segue o que é conhecido como o padrão assíncrono baseado em tarefas (TAP).

Visão geral do modelo assíncrono

O núcleo da programação assíncrona são os objetos e Task<T> , que modelam operações assíncronasTask. Eles são apoiados async pelas palavras-chave e await . O modelo é bastante simples na maioria dos casos:

  • Para código vinculado a E/S, você aguarda uma operação que retorna um Task ou Task<T> dentro de um async método.
  • Para código vinculado à CPU, você aguarda uma operação que é iniciada em um thread em segundo plano com o Task.Run método.

A await palavra-chave é onde a magia acontece. Ele produz controle para o chamador do método que executou awaite, finalmente, permite que uma interface do usuário seja responsiva ou que um serviço seja elástico. Embora existam maneiras de abordar o código assíncrono diferente de async e await, este artigo se concentra nas construções de nível de linguagem.

Nota

Em alguns dos exemplos System.Net.Http.HttpClient a seguir, a classe é usada para baixar alguns dados de um serviço Web. O s_httpClient objeto usado nestes exemplos é um campo estático de Program classe (verifique o exemplo completo):

private static readonly HttpClient s_httpClient = new();

Exemplo vinculado a E/S: Baixar dados de um serviço Web

Talvez seja necessário baixar alguns dados de um serviço Web quando um botão é pressionado, mas não deseja bloquear o thread da interface do usuário. Pode ser realizado assim:

s_downloadButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI as the request
    // from the web service is happening.
    //
    // The UI thread is now free to perform other work.
    var stringData = await s_httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};

O código expressa a intenção (baixar dados de forma assíncrona) sem ficar atolado na interação com Task objetos.

Exemplo ligado à CPU: Executar um cálculo para um jogo

Digamos que você está escrevendo um jogo para celular onde pressionar um botão pode causar dano em muitos inimigos na tela. Realizar o cálculo de danos pode ser caro, e fazê-lo no thread da interface do usuário faria com que o jogo parecesse pausar à medida que o cálculo é realizado!

A melhor maneira de lidar com isso é iniciar um thread em segundo plano, que faz o trabalho usando Task.Run, e aguardar seu resultado usando await. Isso permite que a interface do usuário pareça suave à medida que o trabalho está sendo feito.

static DamageResult CalculateDamageDone()
{
    return new DamageResult()
    {
        // Code omitted:
        //
        // Does an expensive calculation and returns
        // the result of that calculation.
    };
}

s_calculateButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI while CalculateDamageDone()
    // performs its work. The UI thread is free to perform other work.
    var damageResult = await Task.Run(() => CalculateDamageDone());
    DisplayDamage(damageResult);
};

Esse código expressa claramente a intenção do evento de clique do botão, não requer o gerenciamento manual de um thread em segundo plano e o faz de forma sem bloqueio.

O que acontece debaixo das cobertas

No lado C# das coisas, o compilador transforma seu código em uma máquina de estado que acompanha coisas como produzir execução quando um await é atingido e retomar a execução quando um trabalho em segundo plano é concluído.

Para os teoricamente inclinados, esta é uma implementação do Modelo de Promessa de assincronia.

Peças-chave para entender

  • O código assíncrono pode ser usado para código vinculado a E/S e CPU, mas de forma diferente para cada cenário.
  • O código assíncrono usa Task<T> e Task, que são construções usadas para modelar o trabalho que está sendo feito em segundo plano.
  • A async palavra-chave transforma um método em um método assíncrono, que permite que você use a await palavra-chave em seu corpo.
  • Quando a await palavra-chave é aplicada, ela suspende o método de chamada e produz o controle de volta para seu chamador até que a tarefa esperada seja concluída.
  • await só pode ser usado dentro de um método assíncrono.

Reconhecer trabalho ligado à CPU e E/S

Os dois primeiros exemplos deste guia mostraram como você pode usar async e await para trabalho vinculado a E/S e CPU. É fundamental que você possa identificar quando um trabalho que você precisa fazer está vinculado a E/S ou à CPU, porque isso pode afetar muito o desempenho do seu código e pode levar ao uso indevido de certas construções.

Aqui estão duas perguntas que você deve fazer antes de escrever qualquer código:

  1. Seu código estará "esperando" por algo, como dados de um banco de dados?

    Se a sua resposta for "sim", então o seu trabalho está vinculado a E/S.

  2. Seu código estará executando um cálculo caro?

    Se você respondeu "sim", então seu trabalho está vinculado à CPU.

Se o trabalho que você tem é I/O-bound, use async e await sem Task.Run. Você não deve usar a Biblioteca Paralela de Tarefas.

Se o trabalho que você tem é vinculado à CPU e você se preocupa com a capacidade de resposta, use async e await, mas gere o trabalho em outro thread com Task.Run. Se o trabalho for apropriado para simultaneidade e paralelismo, considere também o uso da Biblioteca Paralela de Tarefas.

Além disso, você deve sempre medir a execução do seu código. Por exemplo, você pode se encontrar em uma situação em que seu trabalho vinculado à CPU não é caro o suficiente em comparação com a sobrecarga de comutadores de contexto ao multithreading. Cada escolha tem sua compensação, e você deve escolher a compensação correta para sua situação.

Mais exemplos

Os exemplos a seguir demonstram várias maneiras de escrever código assíncrono em C#. Eles cobrem alguns cenários diferentes que você pode encontrar.

Extrair dados de uma rede

Esse trecho baixa o HTML da URL fornecida e conta o número de vezes que a cadeia de caracteres ".NET" ocorre no HTML. Ele usa ASP.NET para definir um método de controlador de API da Web, que executa essa tarefa e retorna o número.

Nota

Se você planeja fazer análise HTML no código de produção, não use expressões regulares. Em vez disso, use uma biblioteca de análise.

[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
    // Suspends GetDotNetCount() to allow the caller (the web server)
    // to accept another request, rather than blocking on this one.
    var html = await s_httpClient.GetStringAsync(URL);
    return Regex.Matches(html, @"\.NET").Count;
}

Aqui está o mesmo cenário escrito para um aplicativo universal do Windows, que executa a mesma tarefa quando um botão é pressionado:

private readonly HttpClient _httpClient = new HttpClient();

private async void OnSeeTheDotNetsButtonClick(object sender, RoutedEventArgs e)
{
    // Capture the task handle here so we can await the background task later.
    var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://dotnetfoundation.org");

    // Any other work on the UI thread can be done here, such as enabling a Progress Bar.
    // This is important to do here, before the "await" call, so that the user
    // sees the progress bar before execution of this method is yielded.
    NetworkProgressBar.IsEnabled = true;
    NetworkProgressBar.Visibility = Visibility.Visible;

    // The await operator suspends OnSeeTheDotNetsButtonClick(), returning control to its caller.
    // This is what allows the app to be responsive and not block the UI thread.
    var html = await getDotNetFoundationHtmlTask;
    int count = Regex.Matches(html, @"\.NET").Count;

    DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";

    NetworkProgressBar.IsEnabled = false;
    NetworkProgressBar.Visibility = Visibility.Collapsed;
}

Aguarde a conclusão de várias tarefas

Você pode se encontrar em uma situação em que precisa recuperar vários dados simultaneamente. A Task API contém dois métodos Task.WhenAll e Task.WhenAny, que permitem escrever código assíncrono que executa uma espera sem bloqueio em vários trabalhos em segundo plano.

Este exemplo mostra como você pode obter User dados para um conjunto de userIds.

private static async Task<User> GetUserAsync(int userId)
{
    // Code omitted:
    //
    // Given a user Id {userId}, retrieves a User object corresponding
    // to the entry in the database with {userId} as its Id.

    return await Task.FromResult(new User() { id = userId });
}

private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
    var getUserTasks = new List<Task<User>>();
    foreach (int userId in userIds)
    {
        getUserTasks.Add(GetUserAsync(userId));
    }

    return await Task.WhenAll(getUserTasks);
}

Aqui está outra maneira de escrever isso de forma mais sucinta, usando o LINQ:

private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
    var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
    return await Task.WhenAll(getUserTasks);
}

Embora seja menos código, tenha cuidado ao misturar LINQ com código assíncrono. Como o LINQ usa execução adiada (preguiçosa), as chamadas assíncronas não acontecerão imediatamente como acontece em um foreach loop, a menos que você force a sequência gerada a iterar com uma chamada para .ToList() ou .ToArray(). O exemplo acima usa Enumerable.ToArray para executar a consulta ansiosamente e armazenar os resultados em uma matriz. Isso força o código id => GetUserAsync(id) a ser executado e iniciar a tarefa.

Informações e conselhos importantes

Com a programação assíncrona, existem alguns detalhes a ter em mente que podem evitar comportamentos inesperados.

  • asyncos métodos precisam ter um await palavra-chave em seu corpo ou eles nunca vão ceder!

    Isto é importante ter em mente. Se await não for usado no corpo de um async método, o compilador C# gera um aviso, mas o código compila e é executado como se fosse um método normal. Isso é incrivelmente ineficiente, pois a máquina de estado gerada pelo compilador C# para o método assíncrono não está realizando nada.

  • Adicione "Async" como o sufixo de cada nome de método assíncrono que você escrever.

    Esta é a convenção usada no .NET para diferenciar mais facilmente métodos síncronos e assíncronos. Certos métodos que não são explicitamente chamados pelo seu código (como manipuladores de eventos ou métodos de controlador da Web) não se aplicam necessariamente. Como eles não são explicitamente chamados pelo seu código, ser explícito sobre sua nomenclatura não é tão importante.

  • async voidsó deve ser usado para manipuladores de eventos.

    async void é a única maneira de permitir que manipuladores de eventos assíncronos funcionem porque os eventos não têm tipos de Task retorno (portanto, não podem usar e Task<T>). Qualquer outra utilização não async void segue o modelo da TAP e pode ser difícil de utilizar, tais como:

    • As exceções lançadas em um async void método não podem ser capturadas fora desse método.
    • async void os métodos são difíceis de testar.
    • async void Os métodos podem causar efeitos colaterais ruins se o chamador não estiver esperando que eles sejam assíncronos.
  • Siga com cuidado ao usar lambdas assíncronas em expressões LINQ

    As expressões do Lambda no LINQ usam execução diferida, o que significa que o código pode acabar sendo executado em um momento em que você não está esperando. A introdução de tarefas de bloqueio pode facilmente resultar em um impasse se não for escrito corretamente. Além disso, o aninhamento de código assíncrono como este também pode tornar mais difícil raciocinar sobre a execução do código. Async e LINQ são poderosos, mas devem ser usados juntos com o máximo de cuidado e clareza possível.

  • Escreva o código que aguarda as Tarefas de forma sem bloqueio

    Bloquear o thread atual como um meio de aguardar a conclusão de um Task pode resultar em deadlocks e threads de contexto bloqueados e pode exigir um tratamento de erros mais complexo. A tabela a seguir fornece orientações sobre como lidar com a espera por tarefas de forma sem bloqueio:

    Utilize... Em vez disso... Ao desejar fazer isso...
    await Task.Wait ou Task.Result Recuperando o resultado de uma tarefa em segundo plano
    await Task.WhenAny Task.WaitAny Aguardando a conclusão de qualquer tarefa
    await Task.WhenAll Task.WaitAll Aguardando a conclusão de todas as tarefas
    await Task.Delay Thread.Sleep À espera de um período de tempo
  • Pondere a possibilidade de utilizar ValueTask sempre que possível

    Retornar um Task objeto de métodos assíncronos pode introduzir gargalos de desempenho em determinados caminhos. Task é um tipo de referência, portanto, usá-lo significa alocar um objeto. Nos casos em que um método declarado com o async modificador retorna um resultado armazenado em cache ou é concluído de forma síncrona, as alocações extras podem se tornar um custo de tempo significativo em seções críticas de desempenho do código. Pode tornar-se dispendioso se essas atribuições ocorrerem em circuitos apertados. Para obter mais informações, consulte Tipos de retorno assíncrono generalizado.

  • Considere a utilização de ConfigureAwait(false)

    Uma pergunta comum é: "quando devo usar o Task.ConfigureAwait(Boolean) método?". O método permite que uma Task instância configure seu awaiter. Esta é uma consideração importante e defini-la incorretamente pode potencialmente ter implicações de desempenho e até mesmo impasses. Para obter mais informações sobre ConfigureAwaito , consulte as Perguntas frequentes sobre o ConfigureAwait.

  • Escreva código com menos estado

    Não dependa do estado de objetos globais ou da execução de determinados métodos. Em vez disso, dependa apenas dos valores de retorno dos métodos. Porquê?

    • Código será mais fácil de raciocinar.
    • O código será mais fácil de testar.
    • Misturar código assíncrono e síncrono é muito mais simples.
    • Normalmente, as condições de corrida podem ser completamente evitadas.
    • Dependendo dos valores de retorno, a coordenação do código assíncrono é simples.
    • (Bônus) funciona muito bem com injeção de dependência.

Uma meta recomendada é alcançar uma Transparência Referencial completa ou quase completa em seu código. Isso resultará em uma base de código previsível, testável e sustentável.

Exemplo completo

O código a seguir é o texto completo do arquivo Program.cs para o exemplo.

using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.AspNetCore.Mvc;

class Button
{
    public Func<object, object, Task>? Clicked
    {
        get;
        internal set;
    }
}

class DamageResult
{
    public int Damage
    {
        get { return 0; }
    }
}

class User
{
    public bool isEnabled
    {
        get;
        set;
    }

    public int id
    {
        get;
        set;
    }
}

public class Program
{
    private static readonly Button s_downloadButton = new();
    private static readonly Button s_calculateButton = new();

    private static readonly HttpClient s_httpClient = new();

    private static readonly IEnumerable<string> s_urlList = new string[]
    {
            "https://learn.microsoft.com",
            "https://learn.microsoft.com/aspnet/core",
            "https://learn.microsoft.com/azure",
            "https://learn.microsoft.com/azure/devops",
            "https://learn.microsoft.com/dotnet",
            "https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio",
            "https://learn.microsoft.com/education",
            "https://learn.microsoft.com/shows/net-core-101/what-is-net",
            "https://learn.microsoft.com/enterprise-mobility-security",
            "https://learn.microsoft.com/gaming",
            "https://learn.microsoft.com/graph",
            "https://learn.microsoft.com/microsoft-365",
            "https://learn.microsoft.com/office",
            "https://learn.microsoft.com/powershell",
            "https://learn.microsoft.com/sql",
            "https://learn.microsoft.com/surface",
            "https://dotnetfoundation.org",
            "https://learn.microsoft.com/visualstudio",
            "https://learn.microsoft.com/windows",
            "https://learn.microsoft.com/maui"
    };

    private static void Calculate()
    {
        // <PerformGameCalculation>
        static DamageResult CalculateDamageDone()
        {
            return new DamageResult()
            {
                // Code omitted:
                //
                // Does an expensive calculation and returns
                // the result of that calculation.
            };
        }

        s_calculateButton.Clicked += async (o, e) =>
        {
            // This line will yield control to the UI while CalculateDamageDone()
            // performs its work. The UI thread is free to perform other work.
            var damageResult = await Task.Run(() => CalculateDamageDone());
            DisplayDamage(damageResult);
        };
        // </PerformGameCalculation>
    }

    private static void DisplayDamage(DamageResult damage)
    {
        Console.WriteLine(damage.Damage);
    }

    private static void Download(string URL)
    {
        // <UnblockingDownload>
        s_downloadButton.Clicked += async (o, e) =>
        {
            // This line will yield control to the UI as the request
            // from the web service is happening.
            //
            // The UI thread is now free to perform other work.
            var stringData = await s_httpClient.GetStringAsync(URL);
            DoSomethingWithData(stringData);
        };
        // </UnblockingDownload>
    }

    private static void DoSomethingWithData(object stringData)
    {
        Console.WriteLine("Displaying data: ", stringData);
    }

    // <GetUsersForDataset>
    private static async Task<User> GetUserAsync(int userId)
    {
        // Code omitted:
        //
        // Given a user Id {userId}, retrieves a User object corresponding
        // to the entry in the database with {userId} as its Id.

        return await Task.FromResult(new User() { id = userId });
    }

    private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
    {
        var getUserTasks = new List<Task<User>>();
        foreach (int userId in userIds)
        {
            getUserTasks.Add(GetUserAsync(userId));
        }

        return await Task.WhenAll(getUserTasks);
    }
    // </GetUsersForDataset>

    // <GetUsersForDatasetByLINQ>
    private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
    {
        var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
        return await Task.WhenAll(getUserTasks);
    }
    // </GetUsersForDatasetByLINQ>

    // <ExtractDataFromNetwork>
    [HttpGet, Route("DotNetCount")]
    static public async Task<int> GetDotNetCount(string URL)
    {
        // Suspends GetDotNetCount() to allow the caller (the web server)
        // to accept another request, rather than blocking on this one.
        var html = await s_httpClient.GetStringAsync(URL);
        return Regex.Matches(html, @"\.NET").Count;
    }
    // </ExtractDataFromNetwork>

    static async Task Main()
    {
        Console.WriteLine("Application started.");

        Console.WriteLine("Counting '.NET' phrase in websites...");
        int total = 0;
        foreach (string url in s_urlList)
        {
            var result = await GetDotNetCount(url);
            Console.WriteLine($"{url}: {result}");
            total += result;
        }
        Console.WriteLine("Total: " + total);

        Console.WriteLine("Retrieving User objects with list of IDs...");
        IEnumerable<int> ids = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
        var users = await GetUsersAsync(ids);
        foreach (User? user in users)
        {
            Console.WriteLine($"{user.id}: isEnabled={user.isEnabled}");
        }

        Console.WriteLine("Application ending.");
    }
}

// Example output:
//
// Application started.
// Counting '.NET' phrase in websites...
// https://learn.microsoft.com: 0
// https://learn.microsoft.com/aspnet/core: 57
// https://learn.microsoft.com/azure: 1
// https://learn.microsoft.com/azure/devops: 2
// https://learn.microsoft.com/dotnet: 83
// https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio: 31
// https://learn.microsoft.com/education: 0
// https://learn.microsoft.com/shows/net-core-101/what-is-net: 42
// https://learn.microsoft.com/enterprise-mobility-security: 0
// https://learn.microsoft.com/gaming: 0
// https://learn.microsoft.com/graph: 0
// https://learn.microsoft.com/microsoft-365: 0
// https://learn.microsoft.com/office: 0
// https://learn.microsoft.com/powershell: 0
// https://learn.microsoft.com/sql: 0
// https://learn.microsoft.com/surface: 0
// https://dotnetfoundation.org: 16
// https://learn.microsoft.com/visualstudio: 0
// https://learn.microsoft.com/windows: 0
// https://learn.microsoft.com/maui: 6
// Total: 238
// Retrieving User objects with list of IDs...
// 1: isEnabled= False
// 2: isEnabled= False
// 3: isEnabled= False
// 4: isEnabled= False
// 5: isEnabled= False
// 6: isEnabled= False
// 7: isEnabled= False
// 8: isEnabled= False
// 9: isEnabled= False
// 0: isEnabled= False
// Application ending.

Outros recursos