Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Se o código implementar cenários associados a E/S para dar suporte a solicitações de dados de rede, acesso ao banco de dados ou leitura/gravações do sistema de arquivos, a programação assíncrona será a melhor abordagem. Você também pode escrever código assíncrono para cenários associados à CPU, como cálculos caros.
O C# tem um modelo de programação assíncrona no nível da linguagem que permite que você escreva facilmente código assíncrono sem precisar fazer malabarismo com retornos de chamada ou em conformidade com uma biblioteca que dê suporte à assíncrona. O modelo segue o que é conhecido como TAP (padrão assíncrono baseado em tarefa).
Explorar o modelo de programação assíncrona
Os Task
objetos e Task<T>
representam o núcleo da programação assíncrona. Esses objetos são usados para modelar operações assíncronas por meio do suporte às palavras-chave async
e await
. Na maioria dos casos, o modelo é bastante simples para cenários associados a E/S e CPU. Dentro de um método async
:
-
O código associado a E/S inicia uma operação representada por um
Task
ouTask<T>
objeto dentro doasync
método. - O código associado à CPU inicia uma operação em um thread em segundo plano com o método Task.Run.
Em ambos os casos, um ativo Task
representa uma operação assíncrona que pode não ser concluída.
É na palavra-chave await
que a mágica acontece. Ele gera controle ao chamador do método que contém a await
expressão e, por fim, permite que a interface do usuário seja responsiva ou um serviço seja elástico. Embora existam maneiras de abordar código assíncrono além do uso das expressões async
e await
, este artigo se concentra nos construtos ao nível da linguagem.
Observação
Alguns exemplos apresentados neste artigo usam a System.Net.Http.HttpClient classe para baixar dados de um serviço Web. No código de exemplo, o s_httpClient
objeto é um campo estático da classe de tipo Program
:
private static readonly HttpClient s_httpClient = new();
Para obter mais informações, consulte o código de exemplo completo no final deste artigo.
Examinar conceitos subjacentes
Quando você implementa a programação assíncrona no código C#, o compilador transforma seu programa em um computador de estado. Esse constructo rastreia várias operações e estado em seu código, como gerar execução quando o código atinge uma await
expressão e retomar a execução quando um trabalho em segundo plano é concluído.
Em termos de teoria da ciência da computação, a programação assíncrona é uma implementação do modelo Promise de assíncrona.
No modelo de programação assíncrona, há vários conceitos importantes para entender:
- Você pode usar código assíncrono para código associado a E/S e CPU, mas a implementação é diferente.
- O código assíncrono usa
Task<T>
eTask
objetos como constructos para modelar o trabalho em execução em segundo plano. - A
async
palavra-chave declara um método como um método assíncrono, que permite que você use aawait
palavra-chave no corpo do método. - Quando você aplica a
await
palavra-chave, o código suspende o método de chamada e retorna o controle ao chamador até que a tarefa seja concluída. - Você só pode usar a
await
expressão em um método assíncrono.
Exemplo vinculado à E/S: baixar dados de um serviço Web
Neste exemplo, quando o usuário seleciona um botão, o aplicativo baixa dados de um serviço Web. Você não deseja bloquear o thread da interface do usuário do aplicativo durante o processo de download. O código a seguir realiza esta tarefa:
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 alguns dados de forma assíncrona) sem se prender à interação com objetos Task
.
Exemplo limitado pela CPU: executar o cálculo do jogo
No exemplo a seguir, um jogo móvel causa danos a vários agentes na tela em resposta a um evento de botão. Executar o cálculo de danos pode ser caro. Executar o cálculo no thread da interface do usuário pode causar problemas de exibição e interação com a interface do usuário durante o cálculo.
A melhor maneira de lidar com a tarefa é começar um thread de segundo plano usando o método Task.Run
para concluir o trabalho. A operação é realizada usando uma expressão await
. A operação é retomada quando a tarefa é concluída. Essa abordagem permite que a interface do usuário seja executada sem problemas enquanto o trabalho é concluído em segundo plano.
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);
};
O código expressa claramente a intenção do evento de botão Clicked
. Não é necessário o gerenciamento manual de uma thread em segundo plano e ele conclui a tarefa de maneira não bloqueante.
Reconhecer cenários associados à CPU e associados à E/S
Os exemplos anteriores demonstram como usar o modificador async
e a expressão await
para trabalhos limitados por E/S e CPU. Um exemplo para cada cenário mostra como o código é diferente com base no local em que a operação está associada. Para se preparar para sua implementação, você precisa entender como identificar quando uma operação está associada a E/S ou associada à CPU. Sua escolha de implementação pode afetar muito o desempenho do código e potencialmente levar a construções de uso indevido.
Há duas perguntas principais a serem abordadas antes de escrever qualquer código:
Pergunta | Cenário | Implementação |
---|---|---|
O código deve aguardar um resultado ou ação, como dados de um banco de dados? | Limite de E/S | Use o modificador async e a expressão await sem o método Task.Run . Evite usar a Biblioteca Paralela de Tarefas. |
O código deve executar uma computação cara? | Limitado pela CPU | Use o modificador async e expressão await , mas desencadeie o trabalho em outra linha de execução com o método Task.Run . Essa abordagem aborda preocupações com a capacidade de resposta da CPU. Se o trabalho for adequado para a simultaneidade e paralelismo, você também deverá considerar o uso da Biblioteca de paralelismo de tarefas. |
Sempre meça a execução do código. Você pode descobrir que seu trabalho associado à CPU não é caro o suficiente em comparação com a sobrecarga de alternâncias de contexto ao fazer multithreading. Todas as opções têm compensações. Escolha a compensação correta para sua situação.
Explorar outros exemplos
Os exemplos nesta seção demonstram várias maneiras de escrever código assíncrono em C#. Eles abrangem alguns cenários que você pode encontrar.
Extrair dados de uma rede
O código a seguir baixa HTML de uma determinada URL e conta o número de vezes que a cadeia de caracteres ".NET" ocorre no HTML. O código usa ASP.NET para definir um método de controlador de API Web, que executa a tarefa e retorna a contagem.
Observação
Se você pretende fazer análise de HTML no código de produção, não use expressões regulares. 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;
}
Você pode escrever um código semelhante para um Aplicativo Universal do Windows e executar a tarefa de contagem depois que um botão for 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.
// It's important to do the extra work here before the "await" call,
// so 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 action 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;
}
Aguardar a conclusão de várias tarefas
Em alguns cenários, o código precisa recuperar várias partes de dados simultaneamente. As Task
APIs fornecem métodos que permitem que você escreva código assíncrono que executa uma espera sem bloqueio em vários trabalhos em segundo plano:
- método Task.WhenAll
- método Task.WhenAny
O exemplo a seguir mostra como você pode capturar dados de User
objeto para um conjunto de userId
objetos.
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);
}
Você pode escrever esse código de forma mais sucinta usando LINQ:
private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
Embora você escreva menos código usando LINQ, tenha cuidado ao misturar LINQ com código assíncrono. O LINQ usa a execução adiada (ou lenta). Chamadas assíncronas não acontecem imediatamente como ocorrem em um loop foreach
, a menos que você force a sequência gerada a iterar com uma chamada para o método .ToList()
ou .ToArray()
. Este exemplo usa o Enumerable.ToArray método para executar a consulta ansiosamente e armazenar os resultados em uma matriz. Essa abordagem força a instrução id => GetUserAsync(id)
a executar e iniciar a tarefa.
Examinar considerações sobre programação assíncrona
Com a programação assíncrona, há vários detalhes a serem considerados que podem impedir um comportamento inesperado.
Usar await dentro do corpo do método async()
Ao usar o async
modificador, você deve incluir uma ou mais await
expressões no corpo do método. Se o compilador não encontrar uma await
expressão, o método não produzirá. Embora o compilador gere um aviso, o código ainda é compilado e o compilador executa o método. O computador de estado gerado pelo compilador C# para o método assíncrono não realiza nada, portanto, todo o processo é altamente ineficiente.
Adicionar sufixo "Async" a nomes de método assíncrono
A convenção de estilo .NET é adicionar o sufixo "Assíncrono" a todos os nomes de método assíncrono. Essa abordagem ajuda a diferenciar mais facilmente entre métodos síncronos e assíncronos. Determinados métodos que não são explicitamente chamados pelo código (como manipuladores de eventos ou métodos de controlador web) não se aplicam necessariamente nesse cenário. Como esses itens não são explicitamente chamados pelo código, o uso de nomenclatura explícita não é tão importante.
Exibir "async void" somente de manipuladores de eventos
Os manipuladores de eventos devem declarar void
tipos de retorno e não podem usar ou retornar Task
e Task<T>
objetos como outros métodos fazem. Ao escrever manipuladores de eventos assíncronos, você precisa usar o async
modificador em um método que retorne void
para os manipuladores. Outras implementações de métodos de async void
retorno não seguem o modelo TAP e podem apresentar desafios:
- Exceções geradas em um
async void
método não podem ser capturadas fora desse método -
async void
métodos são difíceis de testar -
async void
métodos podem causar efeitos colaterais negativos se quem os invoca não espera que eles sejam assíncronos
Tenha cuidado com lambdas assíncronas no LINQ
É importante ter cuidado ao implementar lambdas assíncronas em expressões LINQ. As expressões Lambda no LINQ usam a execução adiada, o que significa que o código pode ser executado em um momento inesperado. A introdução de tarefas de bloqueio nesse cenário pode facilmente resultar em um deadlock, se o código não for gravado corretamente. Além disso, o aninhamento de código assíncrono também pode dificultar a ponderação a respeito da execução do código. Técnicas Async e LINQ são poderosas, mas devem ser utilizadas juntas da forma mais cuidadosa e clara possível.
Suspender para tarefas de maneira não bloqueadora
Se o programa precisar do resultado de uma tarefa, escreva o código que implemente a await
expressão de maneira não bloqueante. Bloquear o thread atual como um meio de aguardar de forma síncrona para que um item de Task
seja concluído pode resultar em deadlocks e threads de contexto bloqueados. Essa abordagem de programação pode exigir tratamento de erros mais complexo. A tabela a seguir fornece diretrizes sobre como acessar resultados de tarefas de maneira não bloqueante.
Cenário de tarefa | Código atual | Substituir por 'await' |
---|---|---|
Recuperar o resultado de uma tarefa em segundo plano |
Task.Wait ou Task.Result |
await |
Continuar quando qualquer tarefa for concluída | Task.WaitAny |
await Task.WhenAny |
Continuar quando todas as tarefas forem concluídas | Task.WaitAll |
await Task.WhenAll |
Continuar após algum tempo | Thread.Sleep |
await Task.Delay |
Considerar o uso do tipo ValueTask
Quando um método assíncrono retorna um Task
objeto, gargalos de desempenho podem ser introduzidos em determinados caminhos. Como Task
é um tipo de referência, um Task
objeto é alocado do heap. Se um método declarado com o async
modificador retornar um resultado armazenado em cache ou for concluído de forma síncrona, as alocações extras poderão acumular custos de tempo significativos em seções críticas de desempenho do código. Esse cenário pode se tornar caro quando as alocações ocorrem em loops apertados. Para obter mais informações, consulte Tipos de retorno assíncronos generalizados.
Entender quando definir ConfigureAwait(false)
Os desenvolvedores geralmente perguntam sobre quando usar o Task.ConfigureAwait(Boolean) booliano. Essa API permite que uma Task
instância configure o contexto para o computador de estado que implementa qualquer await
expressão. Quando o booliano não está definido corretamente, o desempenho pode degradar ou deadlocks podem ocorrer. Para obter mais informações, consulte As perguntas frequentes sobre ConfigureAwait.
Gravar código sem estado
Evite escrever código que dependa do estado dos objetos globais ou da execução de determinados métodos. Em vez disso, depender apenas dos valores retornados dos métodos. Há muitos benefícios ao escrever código menos dependente de estado.
- Mais fácil de raciocinar sobre código
- Mais fácil de testar código
- Mais simples de misturar código assíncrono e síncrono
- Capaz de evitar condições de corrida no código
- Simples de coordenar o código assíncrono que depende de valores de retorno
- (Bônus) Funciona bem com a injeção de dependência no código
Uma meta recomendada é alcançar a Transparência referencial completa ou quase completa em seu código. Essa abordagem resulta em uma base de código previsível, testável e mantenedível.
Examinar o exemplo completo
O código a seguir representa o exemplo completo, que está disponível no arquivo de exemplo Program.cs .
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.