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.

arp
fonte

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.

Stephen Cleary
fonte
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.

Richard
fonte
-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.

James Wierzba
fonte
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.

Jonesopolis
fonte
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