Llamar a múltiples servicios asíncronos en paralelo

17

Tengo pocos servicios REST asíncronos que no dependen unos de otros. Es decir, mientras "espero" una respuesta del Servicio1, puedo llamar al Servicio2, al Servicio3, etc.

Por ejemplo, consulte el siguiente código:

var service1Response = await HttpService1Async();
var service2Response = await HttpService2Async();

// Use service1Response and service2Response

Ahora, service2Responseno depende service1Responsey se pueden obtener de forma independiente. Por lo tanto, no es necesario que espere la respuesta del primer servicio para llamar al segundo servicio.

No creo que pueda usar Parallel.ForEachaquí, ya que no es una operación vinculada a la CPU.

Para llamar a estas dos operaciones en paralelo, ¿puedo llamar al uso Task.WhenAll? Un problema que veo usando Task.WhenAlles que no devuelve resultados. Para obtener el resultado, ¿puedo llamar task.Resultdespués de llamar Task.WhenAll, ya que todas las tareas ya se han completado y todo lo que necesito para obtener una respuesta?

Código de muestra:

var task1 = HttpService1Async();
var task2 = HttpService2Async();

await Task.WhenAll(task1, task2)

var result1 = task1.Result;
var result2 = task2.Result;

// Use result1 and result2

¿Es este código mejor que el primero en términos de rendimiento? ¿Algún otro enfoque que pueda usar?

Ankit Vijay
fuente
I do not think I can use Parallel.ForEach here since it is not CPU bound operation- No veo la lógica allí. La concurrencia es concurrencia.
Robert Harvey
3
@RobertHarvey Supongo que la preocupación es que, en este contexto, Parallel.ForEachgeneraría nuevos hilos mientras async awaitque haría todo en un solo hilo.
MetaFight
@Ankit depende de uno cuando sea apropiado que bloquee su código. Su segundo ejemplo se bloquearía hasta que ambas respuestas estén listas. Su primer ejemplo, presumiblemente, solo se bloquearía lógicamente cuando el código intentara usar la respuesta ( await) antes de que esté listo.
MetaFight
Puede ser más fácil darle una respuesta más satisfactoria si proporciona un ejemplo menos abstracto del código que consume ambas respuestas de servicio.
MetaFight
@MetaFight En mi segundo ejemplo, lo estoy haciendo WhenAllantes de hacerlo Resultcon la idea de que completa todas las tareas antes de que se llame a .Result. Dado que Task.Result bloquea el hilo de llamada, supongo que si lo llamo después de que las tareas se hayan completado, devolverá el resultado inmediatamente. Quiero validar la comprensión.
Ankit Vijay

Respuestas:

17

Un problema que veo usando Task.WhenAll es que no devuelve resultados

Pero devuelve los resultados. Todos estarán en una matriz de un tipo común, por lo que no siempre es útil usar los resultados ya que necesita encontrar el elemento en la matriz que corresponde al Taskresultado para el que desea el resultado, y potencialmente convertirlo en su tipo real, por lo que podría no ser el enfoque más fácil / más legible en este contexto, pero cuando solo desea obtener todos los resultados de cada tarea, y el tipo común es el tipo con el que desea tratarlos, entonces es genial .

Para obtener el resultado, ¿puedo llamar a task.Result después de llamar a Task.WhenAll, ya que todas las tareas ya se han completado y todo lo que necesito para obtener respuesta?

Sí, puedes hacer eso. También podría awaitusarlos ( awaitdesenvolvería la excepción en cualquier tarea fallida, mientras Resultque lanzaría una excepción agregada, pero de lo contrario sería lo mismo).

¿Es este código mejor que el primero en términos de rendimiento?

Realiza las dos operaciones al mismo tiempo, en lugar de una y luego la otra. Que eso sea mejor o peor depende de cuáles sean esas operaciones subyacentes. Si las operaciones subyacentes son "leer un archivo del disco", entonces hacerlas en paralelo probablemente sea más lento, ya que solo hay un cabezal de disco y solo puede estar en un lugar en un momento dado; saltar entre dos archivos será más lento que leer un archivo y luego otro. Por otro lado, si las operaciones son "realizar alguna solicitud de red" (como es el caso aquí), entonces es muy probable que sean más rápidas (al menos hasta un cierto número de solicitudes concurrentes), porque puede esperar una respuesta desde otra computadora de red igual de rápido cuando también hay otra solicitud de red pendiente en curso. Si quieres saber si es así

¿Algún otro enfoque que pueda usar?

Si no es importante para usted que conozca todas las excepciones lanzadas entre todas las operaciones que está haciendo en paralelo en lugar de solo la primera, simplemente puede realizar awaitlas tareas sin WhenAllninguna. Lo único que WhenAllte da es tener una AggregateExceptionexcepción con cada una de las tareas con fallas, en lugar de tirar cuando llegas a la primera tarea con fallas. Es tan simple como:

var task1 = HttpService1Async();
var task2 = HttpService2Async();

var result1 = await task1;
var result2 = await task2;
Servy
fuente
Eso no es ejecutar tareas en simultáneo y mucho menos en paralelo. Estás esperando que cada tarea se complete en orden secuencial. Completamente bien si no te importa el código de rendimiento.
Rick O'Shea
3
@ RickO'Shea Comienza las operaciones secuencialmente. Se iniciará la segunda operación después de que * se inicia la primera operación. Pero comenzar la operación asincrónica debería ser básicamente instantánea (si no lo es, en realidad no es asincrónica, y eso es un error en ese método). Después de comenzar uno, y luego el otro, no continuará hasta que termine el primero y luego el segundo. Como nada espera a que el primero termine antes de comenzar el segundo, nada les impide ejecutarse simultáneamente (que es lo mismo que ellos corriendo en paralelo).
Servy
@Servy No creo que sea cierto. Agregué el registro dentro de dos operaciones asíncronas que tomaron alrededor de un segundo cada una (ambas realizan llamadas http) y luego las llamé como usted sugirió, y efectivamente, la tarea1 comenzó y terminó y luego la tarea2 comenzó y terminó.
Matt Frear
@MattFrear Entonces el método no era asíncrono. Fue sincrónico. Por definición , un método asincrónico regresará de inmediato, en lugar de regresar una vez que la operación haya finalizado.
Servy
@Servy, por definición, esperar significará que espere hasta que finalice la tarea asincrónica antes de ejecutar la siguiente línea. ¿No es así?
Matt Frear
0

Aquí está el método de extensión que hace uso de SemaphoreSlim y permite establecer el grado máximo de paralelismo

    /// <summary>
    /// Concurrently Executes async actions for each item of <see cref="IEnumerable<typeparamref name="T"/>
    /// </summary>
    /// <typeparam name="T">Type of IEnumerable</typeparam>
    /// <param name="enumerable">instance of <see cref="IEnumerable<typeparamref name="T"/>"/></param>
    /// <param name="action">an async <see cref="Action" /> to execute</param>
    /// <param name="maxDegreeOfParallelism">Optional, An integer that represents the maximum degree of parallelism,
    /// Must be grater than 0</param>
    /// <returns>A Task representing an async operation</returns>
    /// <exception cref="ArgumentOutOfRangeException">If the maxActionsToRunInParallel is less than 1</exception>
    public static async Task ForEachAsyncConcurrent<T>(
        this IEnumerable<T> enumerable,
        Func<T, Task> action,
        int? maxDegreeOfParallelism = null)
    {
        if (maxDegreeOfParallelism.HasValue)
        {
            using (var semaphoreSlim = new SemaphoreSlim(
                maxDegreeOfParallelism.Value, maxDegreeOfParallelism.Value))
            {
                var tasksWithThrottler = new List<Task>();

                foreach (var item in enumerable)
                {
                    // Increment the number of currently running tasks and wait if they are more than limit.
                    await semaphoreSlim.WaitAsync();

                    tasksWithThrottler.Add(Task.Run(async () =>
                    {
                        await action(item).ContinueWith(res =>
                        {
                            // action is completed, so decrement the number of currently running tasks
                            semaphoreSlim.Release();
                        });
                    }));
                }

                // Wait for all tasks to complete.
                await Task.WhenAll(tasksWithThrottler.ToArray());
            }
        }
        else
        {
            await Task.WhenAll(enumerable.Select(item => action(item)));
        }
    }

Uso de muestra:

await enumerable.ForEachAsyncConcurrent(
    async item =>
    {
        await SomeAsyncMethod(item);
    },
    5);
Jay Shah
fuente
-2

Puedes usar

Parallel.Invoke(() =>
{
    HttpService1Async();
},
() =>
{   
    HttpService2Async();
});

o

Task task1 = Task.Run(() => HttpService1Async());
Task task2 = Task.Run(() => HttpService2Async());

//If you wish, you can wait for a particular task to return here like this:
task1.Wait();
usuario1451111
fuente
¿Por qué votos negativos?
user1451111