Envolviendo código síncrono en una llamada asincrónica

95

Tengo un método en la aplicación ASP.NET, que consume bastante tiempo para completarse. Una llamada a este método puede ocurrir hasta 3 veces durante una solicitud de usuario, según el estado de la caché y los parámetros que proporciona el usuario. Cada llamada tarda entre 1 y 2 segundos en completarse. El método en sí es una llamada sincrónica al servicio y no hay posibilidad de anular la implementación.
Entonces, la llamada sincrónica al servicio se parece a lo siguiente:

public OutputModel Calculate(InputModel input)
{
    // do some stuff
    return Service.LongRunningCall(input);
}

Y el uso del método es (tenga en cuenta que la llamada al método puede ocurrir más de una vez):

private void MakeRequest()
{
    // a lot of other stuff: preparing requests, sending/processing other requests, etc.
    var myOutput = Calculate(myInput);
    // stuff again
}

Traté de cambiar la implementación por mi parte para proporcionar un trabajo simultáneo de este método, y esto es lo que llegué hasta ahora.

public async Task<OutputModel> CalculateAsync(InputModel input)
{
    return await Task.Run(() =>
    {
        return Calculate(input);
    });
}

Uso (parte del código "hacer otras cosas" se ejecuta simultáneamente con la llamada al servicio):

private async Task MakeRequest()
{
    // do some stuff
    var task = CalculateAsync(myInput);
    // do other stuff
    var myOutput = await task;
    // some more stuff
}

Mi pregunta es la siguiente. ¿Utilizo el enfoque correcto para acelerar la ejecución en la aplicación ASP.NET o estoy haciendo un trabajo innecesario al intentar ejecutar código síncrono de forma asincrónica? ¿Alguien puede explicar por qué el segundo enfoque no es una opción en ASP.NET (si realmente no lo es)? Además, si tal enfoque es aplicable, ¿debo llamar a dicho método de forma asincrónica si es la única llamada que podríamos realizar en este momento (tengo ese caso, cuando no hay otras cosas que hacer mientras espero que se complete)?
La mayoría de los artículos en la red sobre este tema cubren el uso de async-awaitenfoque con el código, que ya proporciona awaitablemétodos, pero ese no es mi caso. aquíes el bonito artículo que describe mi caso, que no describe la situación de las llamadas paralelas, rechazando la opción de ajustar la llamada sincronizada, pero en mi opinión mi situación es exactamente la ocasión para hacerlo.
Gracias de antemano por la ayuda y los consejos.

Eadel
fuente

Respuestas:

116

Es importante hacer una distinción entre dos tipos diferentes de simultaneidad. La simultaneidad asincrónica es cuando tiene varias operaciones asincrónicas en vuelo (y dado que cada operación es asincrónica, ninguna de ellas está usando un hilo ). La simultaneidad paralela es cuando tiene varios subprocesos, cada uno de los cuales realiza una operación separada.

Lo primero que debe hacer es reevaluar este supuesto:

El método en sí es una llamada sincrónica al servicio y no hay posibilidad de anular la implementación.

Si su "servicio" es un servicio web o cualquier otra cosa que esté vinculada a E / S, entonces la mejor solución es escribir una API asincrónica para él.

Continuaré asumiendo que su "servicio" es una operación vinculada a la CPU que debe ejecutarse en la misma máquina que el servidor web.

Si ese es el caso, lo siguiente que hay que evaluar es otra suposición:

Necesito que la solicitud se ejecute más rápido.

¿Estás absolutamente seguro de que eso es lo que necesitas hacer? ¿Hay algún cambio de interfaz que pueda realizar en su lugar, por ejemplo, iniciar la solicitud y permitir que el usuario haga otro trabajo mientras se procesa?

Procederé con la suposición de que sí, realmente necesita hacer que la solicitud individual se ejecute más rápido.

En este caso, deberá ejecutar código paralelo en su servidor web. Definitivamente, esto no se recomienda en general porque el código paralelo usará subprocesos que ASP.NET puede necesitar para manejar otras solicitudes, y al eliminar / agregar subprocesos, eliminará la heurística del grupo de subprocesos de ASP.NET. Entonces, esta decisión tiene un impacto en todo su servidor.

Cuando usa código paralelo en ASP.NET, está tomando la decisión de limitar realmente la escalabilidad de su aplicación web. También puede ver una gran cantidad de cambios de hilo, especialmente si sus solicitudes tienen ráfagas. Recomiendo usar solo código paralelo en ASP.NET si sabe que el número de usuarios simultáneos será bastante bajo (es decir, no un servidor público).

Por lo tanto, si llega hasta aquí y está seguro de que desea realizar un procesamiento paralelo en ASP.NET, tiene un par de opciones.

Uno de los métodos más fáciles es usar Task.Run, muy similar a su código existente. Sin embargo, no recomiendo implementar un CalculateAsyncmétodo ya que eso implica que el procesamiento es asincrónico (que no lo es). En su lugar, use Task.Runen el punto de la llamada:

private async Task MakeRequest()
{
  // do some stuff
  var task = Task.Run(() => Calculate(myInput));
  // do other stuff
  var myOutput = await task;
  // some more stuff
}

Alternativamente, si funciona bien con su código, se puede utilizar el Paralleltipo, es decir, Parallel.For, Parallel.ForEach, o Parallel.Invoke. La ventaja del Parallelcódigo es que el subproceso de solicitud se usa como uno de los subprocesos paralelos y luego se reanuda la ejecución en el contexto del subproceso (hay menos cambios de contexto que en el asyncejemplo):

private void MakeRequest()
{
  Parallel.Invoke(() => Calculate(myInput1),
      () => Calculate(myInput2),
      () => Calculate(myInput3));
}

No recomiendo usar Parallel LINQ (PLINQ) en ASP.NET en absoluto.

Stephen Cleary
fuente
1
Muchas gracias por una respuesta detallada. Una cosa más que quiero aclarar. Cuando comienzo a ejecutar el uso Task.Run, el contenido se ejecuta en otro hilo, que se toma del grupo de hilos. Entonces no hay ninguna necesidad de envolver las llamadas de bloqueo (como la llamada de la mina al servicio) en Runs, porque siempre consumirán un hilo cada una, que se bloqueará durante la ejecución del método. En tal situación, el único beneficio que me queda async-awaiten mi caso es realizar varias acciones a la vez. Por favor corrígeme si estoy equivocado.
Eadel
2
Sí, el único beneficio que obtiene Run(o Parallel) es la concurrencia. Las operaciones siguen bloqueando un hilo. Dado que dice que el servicio es un servicio web, no recomiendo usar Runo Parallel; en su lugar, escriba una API asincrónica para el servicio.
Stephen Cleary
3
@StephenCleary. ¿Qué quieres decir con "... y dado que cada operación es asincrónica, ninguna de ellas está usando un hilo ..." gracias!
alltej
3
@alltej: para código verdaderamente asincrónico, no hay hilo .
Stephen Cleary
4
@JoshuaFrank: No directamente. El código detrás de una API síncrona debe bloquear el hilo de llamada, por definición. Usted puede usar una API asíncrona como BeginGetResponsey empezar a varias de las personas, y luego bloque (sincrónicamente) hasta que todos ellos están completos, pero este tipo de enfoque es difícil - ver blog.stephencleary.com/2012/07/dont-block-on -async-code.html y msdn.microsoft.com/en-us/magazine/mt238404.aspx . Por lo general, es más fácil y limpio de adoptar por asynccompleto, si es posible.
Stephen Cleary
1

Descubrí que el siguiente código puede convertir una tarea para que siempre se ejecute de forma asincrónica

private static async Task<T> ForceAsync<T>(Func<Task<T>> func)
{
    await Task.Yield();
    return await func();
}

y lo he usado de la siguiente manera

await ForceAsync(() => AsyncTaskWithNoAwaits())

Esto ejecutará cualquier tarea de forma asincrónica para que pueda combinarlas en escenarios WhenAll, WhenAny y otros usos.

También puede simplemente agregar Task.Yield () como la primera línea de su código llamado.

kernowcode
fuente