Use async / await com eficiência com ASP.NET Web API


114

Estou tentando usar o async/awaitrecurso ASP.NET em meu projeto de API da Web. Não tenho certeza se isso fará alguma diferença no desempenho do meu serviço Web API. Encontre abaixo o fluxo de trabalho e o código de amostra do meu aplicativo.

Fluxo de Trabalho:

Aplicativo UI → Ponto de extremidade da API da Web (controlador) → Chamar método na camada de serviço da API da Web → Chamar outro serviço da Web externo. (Aqui temos as interações DB, etc.)

Controlador:

public async Task<IHttpActionResult> GetCountries()
{
    var allCountrys = await CountryDataService.ReturnAllCountries();

    if (allCountrys.Success)
    {
        return Ok(allCountrys.Domain);
    }

    return InternalServerError();
}

Camada de serviço:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    var response = _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");

    return Task.FromResult(response);
}

Testei o código acima e está funcionando. Mas não tenho certeza se é o uso correto de async/await. Por favor, compartilhe seus pensamentos.


Respostas:


203

Não tenho certeza se isso fará alguma diferença no desempenho da minha API.

Lembre-se de que o principal benefício do código assíncrono no lado do servidor é a escalabilidade . Isso não fará com que seus pedidos sejam executados com mais rapidez por mágica. Abordo várias considerações sobre "devo usar async" em meu artigo sobre asyncASP.NET .

Acho que seu caso de uso (chamar outras APIs) é adequado para código assíncrono, apenas tenha em mente que "assíncrono" não significa "mais rápido". A melhor abordagem é primeiro tornar sua IU responsiva e assíncrona; isso fará com que seu aplicativo pareça mais rápido, mesmo que seja um pouco mais lento.

No que diz respeito ao código, ele não é assíncrono:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
  var response = _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
  return Task.FromResult(response);
}

Você precisaria de uma implementação verdadeiramente assíncrona para obter os benefícios de escalabilidade de async:

public async Task<BackOfficeResponse<List<Country>>> ReturnAllCountriesAsync()
{
  return await _service.ProcessAsync<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
}

Ou (se a sua lógica neste método realmente for apenas uma passagem):

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountriesAsync()
{
  return _service.ProcessAsync<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
}

Observe que é mais fácil trabalhar de "dentro para fora" do que "de fora para dentro" dessa forma. Em outras palavras, não comece com uma ação de controlador assíncrona e então force os métodos downstream a serem assíncronos. Em vez disso, identifique as operações naturalmente assíncronas (chamando APIs externas, consultas de banco de dados, etc.) e torne-as assíncronas no nível mais baixo primeiro ( Service.ProcessAsync). Em seguida, deixe o asyncfluxo aumentar, tornando as ações do controlador assíncronas como a última etapa.

E sob nenhuma circunstância você deve usar Task.Runneste cenário.


4
Obrigado Stephen por seus valiosos comentários. Eu mudei todos os meus métodos da camada de serviço para serem assíncronos e agora chamo minha chamada REST externa usando o método ExecuteTaskAsync e está funcionando conforme o esperado. Obrigado também pelas postagens do seu blog sobre assíncrono e Tarefas. Realmente me ajudou muito a obter um entendimento inicial.
arp

5
Você poderia explicar por que você não deve usar Task.Run aqui?
Maarten

1
@Maarten: Eu explico (brevemente) no artigo que vinculei . Eu entro em mais detalhes no meu blog .
Stephen Cleary

Eu li seu artigo Stephen e parece evitar mencionar algo. Quando uma solicitação ASP.NET chega e começa, ela está sendo executada em um thread de pool de threads, tudo bem. Mas se ele se tornar assíncrono, sim, ele iniciará o processamento assíncrono e o thread retornará ao pool imediatamente. Mas o trabalho realizado no método assíncrono será executado em um thread de pool de threads!
Hugh


12

Está correto, mas talvez não seja útil.

Como não há nada para esperar - nenhuma chamada para bloquear APIs que podem operar de forma assíncrona - então você está configurando estruturas para rastrear a operação assíncrona (que tem sobrecarga), mas não faz uso desse recurso.

Por exemplo, se a camada de serviço estava realizando operações de banco de dados com Entity Framework que oferece suporte a chamadas assíncronas:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    using (db = myDBContext.Get()) {
      var list = await db.Countries.Where(condition).ToListAsync();

       return list;
    }
}

Você permitiria que o thread de trabalho fizesse outra coisa enquanto o banco de dados era consultado (e, portanto, capaz de processar outra solicitação).

Aguardar tende a ser algo que precisa ir até o fim: é muito difícil retroceder em um sistema existente.


-1

Você não está aproveitando async / await efetivamente porque o thread de solicitação será bloqueado durante a execução do método síncronoReturnAllCountries()

O encadeamento designado para lidar com uma solicitação ficará inativo esperando enquanto ReturnAllCountries()ele funciona.

Se você puder implementar ReturnAllCountries()para ser assíncrono, verá benefícios de escalabilidade. Isso ocorre porque o thread pode ser liberado de volta para o pool de threads do .NET para manipular outra solicitação, enquanto ReturnAllCountries()está em execução. Isso permitiria que seu serviço tivesse um rendimento mais alto, utilizando threads com mais eficiência.


Isso não é verdade. O soquete está conectado, mas o thread que processa a solicitação pode fazer outra coisa, como processar uma solicitação diferente, enquanto aguarda uma chamada de serviço.
Garr Godfrey

-8

Eu mudaria sua camada de serviço para:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    return Task.Run(() =>
    {
        return _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
    }      
}

da forma como a possui, você ainda está executando sua _service.Processchamada de maneira síncrona e obtendo muito pouco ou nenhum benefício em aguardar por ela.

Com essa abordagem, você está agrupando a chamada potencialmente lenta em a Task, iniciando-a e retornando-a para ser aguardada. Agora você tem o benefício de aguardar o Task.


3
De acordo com o link do blog , pude ver que, mesmo se usarmos Task.Run, o método ainda é executado de forma síncrona?
arp

Hmm. Existem muitas diferenças entre o código acima e aquele link. Seu Servicenão é um método assíncrono e não está sendo aguardado na Serviceprópria chamada. Posso entender seu ponto, mas não acredito que se aplique aqui.
Jonesopolis

8
Task.Rundeve ser evitado no ASP.NET. Essa abordagem removeria todos os benefícios de async/ awaite, na verdade, teria um desempenho pior sob carga do que apenas uma chamada síncrona.
Stephen Cleary
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.