Por que devo criar operações WebAPI assíncronas em vez de sincronizar?

109

Eu tenho a seguinte operação em uma API Web que criei:

// GET api/<controller>
[HttpGet]
[Route("pharmacies/{pharmacyId}/page/{page}/{filter?}")]
public CartTotalsDTO GetProductsWithHistory(Guid pharmacyId, int page, string filter = null ,[FromUri] bool refresh = false)
{
    return delegateHelper.GetProductsWithHistory(CustomerContext.Current.GetContactById(pharmacyId), refresh);
}

A chamada para este serviço da web é feita por meio de uma chamada Jquery Ajax desta forma:

$.ajax({
      url: "/api/products/pharmacies/<%# Farmacia.PrimaryKeyId.Value.ToString() %>/page/" + vm.currentPage() + "/" + filter,
      type: "GET",
      dataType: "json",
      success: function (result) {
          vm.items([]);
          var data = result.Products;
          vm.totalUnits(result.TotalUnits);
      }          
  });

Já vi alguns desenvolvedores que implementam a operação anterior desta forma:

// GET api/<controller>
[HttpGet]
[Route("pharmacies/{pharmacyId}/page/{page}/{filter?}")]
public async Task<CartTotalsDTO> GetProductsWithHistory(Guid pharmacyId, int page, string filter = null ,[FromUri] bool refresh = false)
{
    return await Task.Factory.StartNew(() => delegateHelper.GetProductsWithHistory(CustomerContext.Current.GetContactById(pharmacyId), refresh));
}

Devo dizer, porém, que GetProductsWithHistory () é uma operação bastante longa. Dado o meu problema e contexto, como tornar a operação webAPI assíncrona me beneficiará?

David Jiménez Martínez
fonte
1
O lado do cliente usa AJAX, que já é assíncrono. Você não precisa que o serviço também seja escrito como um async Task<T>. Lembre-se, AJAX foi implementado antes mesmo de o TPL existir :)
Dominic Zukiewicz
65
Você precisa entender por que está implementando controladores assíncronos, muitos não. O IIS tem um número limitado de threads disponíveis e, quando todos estão em uso, o servidor não consegue processar novas solicitações. Com controladores assíncronos, quando um processo está aguardando a conclusão de E / S, seu encadeamento é liberado para o servidor usar para processar outras solicitações.
Matija Grcic 02 de
3
Quais desenvolvedores você viu fazer isso? Se houver alguma postagem no blog ou artigo que recomende essa técnica, poste um link.
Stephen Cleary,
3
Você só obtém o benefício total do assíncrono se o seu processo for assíncrono desde o início (incluindo o próprio aplicativo da web e seus controladores) até quaisquer atividades que possam sair de seu processo (incluindo atrasos de temporizador, E / S de arquivo, acesso ao banco de dados, e solicitações da web que faz). Nesse caso, seu ajudante delegado precisa ser GetProductsWithHistoryAsync()devolvido Task<CartTotalsDTO>. Pode haver uma vantagem em gravar seu controlador de forma assíncrona se você pretende migrar as chamadas que ele faz para serem assíncronas também; então você começa a tirar proveito das partes assíncronas conforme migra o resto.
Keith Robertson de
1
Se o processo que você está fazendo está saindo e atingindo o banco de dados, então o seu thread da web está apenas esperando que ele volte e segurando esse thread. Se você atingiu sua contagem máxima de threads e outra solicitação chega, ela tem que esperar. Por que fazer isso? Em vez disso, você desejaria liberar esse thread de seu controlador para que outra solicitação possa usá-lo e só pegue outro thread da web quando sua solicitação original do banco de dados voltar. msdn.microsoft.com/en-us/magazine/dn802603.aspx
user441521

Respostas:

98

Em seu exemplo específico, a operação não é assíncrona, então o que você está fazendo é assíncrono durante a sincronização. Você está apenas liberando um tópico e bloqueando outro. Não há razão para isso, porque todos os threads são threads de pool de threads (ao contrário de um aplicativo GUI).

Em minha discussão sobre “async over sync”, sugeri fortemente que, se você tiver uma API que é implementada internamente de forma síncrona, não deve expor uma contraparte assíncrona que simplesmente envolve o método síncrono Task.Run.

De Devo expor wrappers síncronos para métodos assíncronos?

No entanto, ao fazer chamadas WebAPI asynconde há uma operação assíncrona real (geralmente E / S) em vez de bloquear um thread que fica e espera por um resultado, o thread volta para o pool de threads e, portanto, é capaz de realizar alguma outra operação. Acima de tudo, isso significa que seu aplicativo pode fazer mais com menos recursos e isso melhora a escalabilidade.

i3arnon
fonte
3
@efaruk todos os threads são threads de trabalho. Liberar um thread ThreadPool e bloquear outro é inútil.
i3arnon
1
@efaruk Não tenho certeza do que você está tentando dizer ... mas contanto que você concorde que não há razão para usar async over sync no WebAPI, então está tudo bem.
i3arnon de
@efaruk "async over sync" (ie await Task.Run(() => CPUIntensive())) é inútil no asp.net. Você não ganha nada fazendo isso. Você está apenas liberando um thread ThreadPool para ocupar outro. É menos eficiente do que simplesmente chamar o método síncrono.
i3arnon
1
@efaruk Não, isso não é razoável. Seu exemplo executa as tarefas independentes sequencialmente. Você realmente precisa ler asyc / await antes de fazer recomendações. Você precisaria usar await Task.WhenAllpara executar em paralelo.
Søren Boisen
1
@efaruk Como Boisen explica, seu exemplo não adiciona nenhum valor além de simplesmente chamar esses métodos síncronos sequencialmente. Você pode usar Task.Runse quiser paralelizar sua carga em vários threads, mas não é isso que "async over sync" significa. "async over sync" faz referência à criação de um método assíncrono como um wrapper sobre um síncrono. Você pode ver na citação da minha resposta.
i3arnon
1

Uma abordagem poderia ser (eu usei isso com sucesso em aplicativos de cliente) ter um serviço do Windows executando as operações demoradas com threads de trabalho e, em seguida, fazer isso no IIS para liberar os threads até que a operação de bloqueio seja concluída: Observe, isso pressupõe os resultados são armazenados em uma tabela (linhas identificadas por jobId) e um processo mais limpo limpando-os algumas horas após o uso.

Para responder à pergunta, "Dado meu problema e contexto, como tornar a operação assíncrona da webAPI me beneficiará?" considerando que é "uma operação bastante longa", estou pensando em muitos segundos em vez de ms, essa abordagem libera threads do IIS. Obviamente, você também deve executar um serviço do Windows que consome recursos, mas essa abordagem pode evitar que uma inundação de consultas lentas roube threads de outras partes do sistema.

// GET api/<controller>
[HttpGet]
[Route("pharmacies/{pharmacyId}/page/{page}/{filter?}")]
public async Task<CartTotalsDTO> GetProductsWithHistory(Guid pharmacyId, int page, string filter = null ,[FromUri] bool refresh = false)
{
        var jobID = Guid.NewGuid().ToString()
        var job = new Job
        {
            Id = jobId,
            jobType = "GetProductsWithHistory",
            pharmacyId = pharmacyId,
            page = page,
            filter = filter,
            Created = DateTime.UtcNow,
            Started = null,
            Finished = null,
            User =  {{extract user id in the normal way}}
        };
        jobService.CreateJob(job);

        var timeout = 10*60*1000; //10 minutes
        Stopwatch sw = new Stopwatch();
        sw.Start();
        bool responseReceived = false;
        do
        {
            //wait for the windows service to process the job and build the results in the results table
            if (jobService.GetJob(jobId).Finished == null)
            {
                if (sw.ElapsedMilliseconds > timeout ) throw new TimeoutException();
                await Task.Delay(2000);
            }
            else
            {
                responseReceived = true;
            }
        } while (responseReceived == false);

    //this fetches the results from the temporary results table
    return jobService.GetProductsWithHistory(jobId);
}

fonte