Utilice async / await de forma eficaz con ASP.NET Web API

114

Estoy intentando hacer uso de la async/awaitfunción de ASP.NET en mi proyecto de API web. No estoy muy seguro de si hará alguna diferencia en el rendimiento de mi servicio API web. A continuación, encontrará el flujo de trabajo y el código de muestra de mi aplicación.

Flujo de trabajo:

Aplicación de interfaz de usuario → Punto final de la API web (controlador) → Método de llamada en la capa de servicio de la API web → Llamar a otro servicio web externo. (Aquí tenemos las interacciones DB, etc.)

Controlador:

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

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

    return InternalServerError();
}

Capa de servicio:

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

    return Task.FromResult(response);
}

Probé el código anterior y está funcionando. Pero no estoy seguro de si es el uso correcto de async/await. Por favor comparta sus pensamientos.

arp
fuente

Respuestas:

202

No estoy muy seguro de si hará alguna diferencia en el rendimiento de mi API.

Tenga en cuenta que el principal beneficio del código asincrónico en el lado del servidor es la escalabilidad . No hará mágicamente que sus solicitudes se ejecuten más rápido. Cubro varias consideraciones de "debería usar async" en mi artículo sobre asyncASP.NET .

Creo que su caso de uso (llamar a otras API) es adecuado para el código asincrónico, solo tenga en cuenta que "asincrónico" no significa "más rápido". El mejor enfoque es primero hacer que su interfaz de usuario sea receptiva y asincrónica; esto hará que su aplicación se sienta más rápida incluso si es un poco más lenta.

En lo que respecta al código, esto no es asincrónico:

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

Necesitaría una implementación verdaderamente asincrónica para obtener los beneficios de escalabilidad de async:

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

O (si su lógica en este método realmente es solo una transferencia):

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

Tenga en cuenta que es más fácil trabajar "de adentro hacia afuera" en lugar de "de afuera hacia adentro" de esta manera. En otras palabras, no empiece con una acción de controlador asíncrono y luego fuerce los métodos posteriores a ser asíncronos. En su lugar, identifique las operaciones naturalmente asincrónicas (llamadas a API externas, consultas de bases de datos, etc.) y conviértalas en asincrónicas en el nivel más bajo primero ( Service.ProcessAsync). Luego, deje que asyncfluya, haciendo que las acciones de su controlador sean asincrónicas como último paso.

Y bajo ninguna circunstancia debe utilizar Task.Runen este escenario.

Stephen Cleary
fuente
4
Gracias Stephen por tus valiosos comentarios. Cambié todos mis métodos de capa de servicio para que sean asíncronos y ahora llamo a mi llamada REST externa usando el método ExecuteTaskAsync y está funcionando como se esperaba. También gracias por las publicaciones de su blog sobre async y Tasks. Realmente me ayudó mucho obtener una comprensión inicial.
arp
5
¿Podría explicar por qué no debería utilizar Task.Run aquí?
Maarten
1
@Maarten: Lo explico (brevemente) en el artículo que vinculé . Entro en más detalles en mi blog .
Stephen Cleary
He leído tu artículo Stephen y parece que evita mencionar algo. Cuando llega una solicitud ASP.NET y comienza, se está ejecutando en un subproceso de grupo de subprocesos, está bien. Pero si se vuelve asíncrono, entonces sí, inicia el procesamiento asíncrono y ese hilo regresa al grupo de inmediato. ¡Pero el trabajo realizado en el método asíncrono se ejecutará en un subproceso de grupo de subprocesos!
Hugh
12

Es correcto, pero quizás no sea útil.

Como no hay nada que esperar, no hay llamadas a las API de bloqueo que podrían operar de forma asincrónica, entonces está configurando estructuras para rastrear la operación asincrónica (que tiene una sobrecarga) pero luego no hace uso de esa capacidad.

Por ejemplo, si la capa de servicio estaba realizando operaciones de base de datos con Entity Framework, que admite llamadas asincrónicas:

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

       return list;
    }
}

Permitiría que el hilo de trabajo haga otra cosa mientras se consulta la base de datos (y, por lo tanto, puede procesar otra solicitud).

Await tiende a ser algo que debe reducirse hasta el final: es muy difícil adaptarse a un sistema existente.

Ricardo
fuente
-1

No está aprovechando async / await de manera efectiva porque el hilo de solicitud se bloqueará mientras se ejecuta el método sincrónicoReturnAllCountries()

El hilo que está asignado para manejar una solicitud estará esperando inactivo mientras ReturnAllCountries()funciona.

Si puede implementar ReturnAllCountries()para ser asíncrono, verá beneficios de escalabilidad. Esto se debe a que el subproceso podría liberarse de nuevo al grupo de subprocesos .NET para manejar otra solicitud, mientras ReturnAllCountries()se ejecuta. Esto permitiría que su servicio tuviera un mayor rendimiento al utilizar subprocesos de manera más eficiente.

James Wierzba
fuente
Esto no es verdad. El socket está conectado pero el hilo que procesa la solicitud puede hacer otra cosa, como procesar una solicitud diferente, mientras espera una llamada de servicio.
Garr Godfrey
-8

Cambiaría su capa de servicio a:

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

tal como lo tiene, todavía está ejecutando su _service.Processllamada sincrónicamente y obteniendo muy poco o ningún beneficio al esperarla.

Con este enfoque, envuelve la llamada potencialmente lenta en a Task, la inicia y la devuelve para que la espere. Ahora obtiene el beneficio de esperar el Task.

Jonesopolis
fuente
3
Según el enlace del blog , puedo ver que incluso si usamos Task.Run, ¿el método todavía se ejecuta sincrónicamente?
arp
Hmm. Hay muchas diferencias entre el código anterior y ese enlace. Your Serviceno es un método asíncrono y no se está esperando dentro de la Servicepropia llamada. Puedo entender su punto, pero no creo que se aplique aquí.
Jonesopolis
8
Task.Rundebe evitarse en ASP.NET. Este enfoque eliminaría todos los beneficios de async/ awaity en realidad funcionaría peor bajo carga que solo una llamada síncrona.
Stephen Cleary