Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Se seu código implementa cenários vinculados a E/S para dar suporte a solicitações de dados de rede, acesso ao banco de dados ou leitura/gravação do sistema de arquivos, a programação assíncrona é a melhor abordagem. Você também pode escrever código assíncrono para cenários vinculados à CPU, como cálculos caros.
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. O modelo segue o que é conhecido como o padrão assíncrono baseado em tarefas (TAP).
Explore o modelo de programação assíncrona
Os objetos Task
e Task<T>
representam o núcleo da programação assíncrona. Esses objetos são usados para modelar operações assíncronas, suportando as palavras-chave async
e await
. Na maioria dos casos, o modelo é bastante simples para cenários ligados a E/S e CPU. Dentro de um método async
:
-
código ligado a E/S inicia uma operação representada por um objeto
Task
ouTask<T>
dentro do métodoasync
. - código vinculado à CPU inicia uma operação em um thread em segundo plano com o método Task.Run.
Em ambos os casos, um Task
ativo representa uma operação assíncrona que pode não estar concluída.
A await
palavra-chave é onde a magia acontece. Ele produz controle para o chamador do método que contém a expressão await
e, finalmente, permite que a interface do usuário seja responsiva ou que um serviço seja elástico. Embora existam maneiras de abordar o código assíncrono que não usam as expressões async
e await
, este artigo se concentra nas construções de nível de linguagem.
Nota
Alguns exemplos apresentados neste artigo usam a classe System.Net.Http.HttpClient para baixar dados de um serviço Web. No código de exemplo, o objeto s_httpClient
é um campo estático do tipo Program
classe:
private static readonly HttpClient s_httpClient = new();
Para obter mais informações, consulte o de código de exemplo completo no final deste artigo.
Rever os conceitos subjacentes
Quando você implementa programação assíncrona em seu código C#, o compilador transforma seu programa em uma máquina de estado. Esta construção monitoriza várias operações e estados no seu código, como ceder a execução quando o código alcança uma expressão await
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 assincronia.
No modelo de programação assíncrona, há vários conceitos-chave para entender:
- Você pode usar código assíncrono para código vinculado a E/S e código vinculado à CPU, mas a implementação é diferente.
- O código assíncrono usa objetos
Task<T>
eTask
como construções para modelar o trabalho em execução em segundo plano. - A palavra-chave
async
declara um método como um método assíncrono, que permite que você use a palavra-chaveawait
no corpo do método. - Quando aplica a palavra-chave
await
, o código suspende o método de chamada e devolve o controle ao chamador até que a tarefa seja concluída. - Você só pode usar a expressão
await
em um método assíncrono.
Exemplo vinculado a E/S: Baixar dados do 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 fluxo da interface do aplicativo durante o processo de download. O código a seguir realiza essa 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 dados de forma assíncrona) sem ficar atolado na interação com Task
objetos.
Exemplo limitado pela CPU: Realizar cálculo do jogo
No exemplo seguinte, um jogo móvel inflige dano a vários agentes na tela em resposta a um evento de botão. Realizar o cálculo de danos pode ser caro. A execução do cálculo no thread da interface do usuário pode causar problemas de exibição e interação da interface do usuário durante o cálculo.
A melhor maneira de lidar com a tarefa é iniciar um thread em segundo plano para concluir o trabalho com o método Task.Run
. A operação produz 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 do botão Clicked
. Ele não requer o gerenciamento manual de um thread em segundo plano e conclui a tarefa sem bloqueios.
Reconhecer cenários ligados à CPU e E/S
Os exemplos anteriores demonstram como usar o modificador de async
e a expressão await
para trabalhos ligados a E/S e CPU. Um exemplo para cada cenário mostra como o código é diferente com base em onde a operação está vinculada. Para se preparar para sua implementação, você precisa entender como identificar quando uma operação está ligada a E/S ou CPU. Sua escolha de implementação pode afetar muito o desempenho do seu código e potencialmente levar ao uso indevido de construções.
Há duas perguntas principais a serem respondidas antes de escrever qualquer código:
Pergunta | Cenário | Execução |
---|---|---|
O código deve aguardar um resultado ou ação, como dados de um banco de dados? | ligados a E/S | Use o async modificador e a await expressão sem o método Task.Run . Evite usar a Biblioteca Paralela de Tarefas. |
O código deve executar um cálculo caro? | dependente da CPU | Use o modificador async e a expressão await , mas assuma o trabalho noutro thread com o método Task.Run . Essa abordagem aborda as preocupações com a capacidade de resposta da CPU. Se o trabalho for apropriado para simultaneidade e paralelismo, considere também o uso da Biblioteca Paralela de Tarefas. |
Meça sempre a execução do seu código. Você pode descobrir que o seu trabalho vinculado à CPU não é caro o suficiente em comparação com a sobrecarga das mudanças de contexto quando se utiliza multithreading. Toda escolha tem compensações. Escolha a compensação correta para a sua situação.
Explore outros exemplos
Os exemplos nesta seção demonstram várias maneiras de escrever código assíncrono em C#. Eles cobrem 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.
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;
}
Você pode escrever código semelhante para um Aplicativo Universal do Windows e executar a tarefa de contagem depois de pressionar um botão:
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;
}
Aguarde a conclusão de várias tarefas
Em alguns cenários, o código precisa recuperar várias partes de dados simultaneamente. As APIs Task
fornecem métodos que permitem escrever 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 se pode obter dados de objetos User
para um conjunto de objetos userId
.
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 este 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 execução diferida (ou preguiçosa). As chamadas assíncronas não acontecem imediatamente como acontece num ciclo de foreach
, a menos que a sequência gerada seja forçada a iterar com uma chamada ao método .ToList()
ou .ToArray()
. Este exemplo usa o método Enumerable.ToArray para executar a consulta ansiosamente e armazenar os resultados em uma matriz. Esta abordagem força a instrução id => GetUserAsync(id)
a ser executada e a iniciar a tarefa.
Revisar considerações para programação assíncrona
Com a programação assíncrona, há vários detalhes a ter em mente que podem evitar comportamentos inesperados.
Use `await` dentro do corpo do método `async()`
Ao usar o modificador async
, você deve incluir uma ou mais expressões await
no corpo do método. Se o compilador não encontrar uma expressão await
, o método falha em produzir. Embora o compilador gere um aviso, o código ainda compila e o compilador executa o método. A máquina de estado gerada 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étodos assíncronos
A convenção de estilo .NET é adicionar o sufixo "Async" a todos os nomes de método assíncrono. Essa abordagem ajuda a diferenciar mais facilmente entre 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 neste cenário. Como esses itens não são explicitamente chamados pelo seu código, usar nomenclatura explícita não é tão importante.
Retornar 'async void' somente de manipuladores de eventos
Os manipuladores de eventos devem declarar void
tipos de retorno e não podem usar ou retornar objetos Task
e Task<T>
como outros métodos fazem. Ao escrever manipuladores de eventos assíncronos, você precisa usar o modificador async
em um método de retorno de void
para os manipuladores. Outras implementações de métodos de retorno de async void
não seguem o modelo TAP e podem apresentar desafios:
- Exceções lançadas num método
async void
não podem ser interceptadas fora desse método. - Os métodos
async void
são difíceis de testar. -
async void
métodos podem causar efeitos colaterais negativos se o chamador não estiver preparado para que sejam assíncronos
Tenha cuidado com lambdas assíncronas no LINQ
É importante ter cuidado ao implementar lambdas assíncronos em expressões LINQ. As expressões do Lambda no LINQ usam 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 impasse, se o código não for escrito corretamente. Além disso, o encadeamento de código assíncrono pode também tornar mais difícil compreender a execução do código. Async e LINQ são poderosos, mas essas técnicas devem ser usadas juntas com o maior cuidado e clareza possível.
Rendimento para tarefas de forma sem bloqueio
Se o seu programa precisar do resultado de uma tarefa, escreva um código que implemente a expressão await
de forma sem bloqueio. Bloquear o thread atual como um meio de aguardar de forma síncrona a conclusão de um item Task
pode resultar em deadlocks e threads de contexto bloqueados. Esta abordagem de programação pode exigir um tratamento de erros mais complexo. A tabela a seguir fornece orientação sobre como aceder aos resultados das tarefas de forma não bloqueante.
Cenário de tarefa | Código atual | Substitua por 'aguardar' |
---|---|---|
Recuperar o resultado de uma tarefa em segundo plano |
Task.Wait ou Task.Result |
await |
Continue quando qualquer tarefa for concluída | Task.WaitAny |
await Task.WhenAny |
Continuar quando todas as tarefas forem concluídas | Task.WaitAll |
await Task.WhenAll |
Continue depois de algum tempo | Thread.Sleep |
await Task.Delay |
Considere o uso do tipo ValueTask
Quando um método assíncrono retorna um objeto Task
, gargalos de desempenho podem ser introduzidos em determinados caminhos. Como Task
é um tipo de referência, um objeto Task
é alocado a partir do heap. Se um método declarado com o modificador async
retorna um resultado armazenado em cache ou é concluído de forma síncrona, as alocações extras podem acumular custos de tempo significativos em seções críticas de desempenho do código. Este cenário pode tornar-se dispendioso quando as alocações ocorrem em circuitos apertados. Para obter mais informações, consulte Tipos de retorno assíncrono generalizado.
Compreenda quando definir ConfigureAwait(false)
Os desenvolvedores geralmente perguntam sobre quando usar o Task.ConfigureAwait(Boolean) booleano. Essa API permite que uma instância Task
configure o contexto para a máquina de estado que implementa qualquer expressão await
. Quando o booleano não está definido corretamente, o desempenho pode se degradar ou podem ocorrer impasses. Para obter mais informações, consulte ConfigureAwait FAQ.
Escreva código menos dependente de estado
Evite escrever código que 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. Há muitos benefícios em escrever código menos dependente de estado:
- Mais fácil de raciocinar sobre o código
- Código mais fácil de testar
- Mais simples de misturar código assíncrono e síncrono
- Capaz de evitar condições de competição em código
- Simples de coordenar código assíncrono que depende de valores de retorno
- (Bônus) Funciona bem com injeção de dependência no código
Uma meta recomendada é alcançar uma Transparência Referencial completa ou quase completa no seu código. Essa abordagem resulta em uma base de código previsível, testável e sustentável.
Analise 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.