¿Por qué debería crear operaciones asíncronas de WebAPI en lugar de sincronizar?

109

Tengo la siguiente operación en una API web que creé:

// 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);
}

La llamada a este servicio web se realiza a través de una llamada Jquery Ajax de esta manera:

$.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);
      }          
  });

He visto algunos desarrolladores que implementan la operación anterior de esta manera:

// 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));
}

Sin embargo, debo decir que GetProductsWithHistory () es una operación bastante larga. Dado mi problema y contexto, ¿cómo me beneficiará hacer que la operación webAPI sea asincrónica?

David Jiménez Martínez
fuente
1
El lado del cliente usa AJAX, que ya es asincrónico. No es necesario que el servicio también esté escrito como async Task<T>. Recuerde, AJAX se implementó antes de que existiera el TPL :)
Dominic Zukiewicz
65
Debe comprender por qué está implementando controladores asíncronos, muchos no lo hacen. IIS tiene un número limitado de subprocesos disponibles y cuando todos están en uso, el servidor no puede procesar nuevas solicitudes. Con los controladores asíncronos, cuando un proceso está esperando que se complete la E / S, su hilo se libera para que el servidor lo utilice para procesar otras solicitudes.
Matija Grcic
3
¿Qué desarrolladores has visto hacer eso? Si hay alguna publicación de blog o artículo que recomiende esa técnica, publique un enlace.
Stephen Cleary
3
Solo obtiene el beneficio completo de async si su proceso es compatible con async desde la parte superior (incluida la aplicación web en sí y sus controladores) hasta cualquier actividad de espera que salga de su proceso (incluidos retrasos del temporizador, E / S de archivos, acceso a la base de datos, y solicitudes web que realiza). En este caso, su asistente delegado necesita una GetProductsWithHistoryAsync()devolución Task<CartTotalsDTO>. Puede ser beneficioso escribir su controlador asíncrono si tiene la intención de migrar las llamadas que realiza para que también sean asíncronas; luego comienza a beneficiarse de las partes asincrónicas a medida que migra el resto.
Keith Robertson
1
Si el proceso que está haciendo se apaga y llega a la base de datos, entonces su hilo web solo está esperando a que vuelva y mantenga ese hilo. Si ha alcanzado su número máximo de subprocesos y llega otra solicitud, tiene que esperar. ¿Por qué hacer eso? En su lugar, querrá liberar ese hilo de su controlador para que otra solicitud pueda usarlo y solo tome otro hilo web cuando vuelva su solicitud original de la base de datos. msdn.microsoft.com/en-us/magazine/dn802603.aspx
user441521

Respuestas:

98

En su ejemplo específico, la operación no es asincrónica en absoluto, por lo que lo que está haciendo es asíncrona sobre sincronización. Solo estás liberando un hilo y bloqueando otro. No hay ninguna razón para eso, porque todos los subprocesos son subprocesos de grupo de subprocesos (a diferencia de una aplicación GUI).

En mi análisis de "async over sync", sugerí encarecidamente que si tiene una API que se implementa internamente de forma sincrónica, no debe exponer una contraparte asincrónica que simplemente envuelve el método sincrónico en Task.Run.

Desde En caso expongo envoltorios síncronos de métodos asincrónicos?

Sin embargo, al realizar llamadas WebAPI asyncdonde hay una operación asincrónica real (generalmente E / S) en lugar de bloquear un subproceso que se encuentra y espera un resultado, el subproceso vuelve al grupo de subprocesos y, por lo tanto, puede realizar alguna otra operación. Sobre todo, eso significa que su aplicación puede hacer más con menos recursos y eso mejora la escalabilidad.

i3arnon
fuente
3
@efaruk todos los hilos son hilos de trabajo. Liberar un hilo de ThreadPool y bloquear otro no tiene sentido.
i3arnon
1
@efaruk No estoy seguro de lo que está tratando de decir ... pero siempre que esté de acuerdo, no hay razón para usar async sobre sincronización en WebAPI, entonces está bien.
i3arnon
@efaruk "async over sync" (es decir await Task.Run(() => CPUIntensive())) es inútil en asp.net. No ganas nada al hacer eso. Solo está liberando un hilo ThreadPool para ocupar otro. Es menos eficiente que simplemente llamar al método sincrónico.
i3arnon
1
@efaruk No, eso no es razonable. Su ejemplo ejecuta las tareas independientes secuencialmente. Realmente necesita leer sobre asyc / await antes de hacer recomendaciones. Necesitaría usar await Task.WhenAllpara ejecutar en paralelo.
Søren Boisen
1
@efaruk Como explica Boisen, su ejemplo no agrega ningún valor además de simplemente llamar a estos métodos síncronos secuencialmente. Puede usar Task.Runsi desea paralelizar su carga en varios subprocesos, pero eso no es lo que significa "async over sync". "async over sync" hace referencia a la creación de un método asincrónico como un contenedor sobre uno sincrónico. Puede ver el en la cita en mi respuesta.
i3arnon
1

Un enfoque podría ser (he usado esto con éxito en aplicaciones de clientes) para que un servicio de Windows ejecute las operaciones largas con subprocesos de trabajo, y luego hacer esto en IIS para liberar los subprocesos hasta que se complete la operación de bloqueo: Tenga en cuenta, esto supone Los resultados se almacenan en una tabla (filas identificadas por jobId) y un proceso más limpio los limpia algunas horas después de su uso.

Para responder a la pregunta, "Dado mi problema y contexto, ¿cómo me beneficiará hacer que la operación webAPI sea asincrónica?" Dado que es "una operación bastante larga", creo que muchos segundos en lugar de ms, este enfoque libera subprocesos de IIS. Obviamente, también debe ejecutar un servicio de Windows que a su vez consume recursos, pero este enfoque podría evitar que una avalancha de consultas lentas robe hilos de otras partes del 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);
}

fuente