web-dev-qa-db-fra.com

Appel de plusieurs services asynchrones en parallèle

J'ai peu de services asynchrones REST qui ne dépendent pas les uns des autres. En attendant "une réponse" de Service1, je peux appeler Service2, Service3 et ainsi de suite.

Par exemple, reportez-vous au code ci-dessous:

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

// Use service1Response and service2Response

Maintenant, service2Response ne dépend pas de service1Response et ils peuvent être récupérés indépendamment. Par conséquent, il n'est pas nécessaire que j'attende la réponse du premier service pour appeler le deuxième service.

Je ne pense pas pouvoir utiliser Parallel.ForEach ici car il ne s'agit pas d'une opération liée au processeur.

Pour appeler ces deux opérations en parallèle, puis-je appeler use Task.WhenAll? Un problème que je vois en utilisant Task.WhenAll est qu'il ne renvoie pas de résultats. Pour récupérer le résultat, puis-je appeler task.Result après avoir appelé Task.WhenAll, puisque toutes les tâches sont déjà terminées et tout ce dont j'ai besoin pour obtenir une réponse?

Exemple de code:

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

await Task.WhenAll(task1, task2)

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

// Use result1 and result2

Ce code est-il meilleur que le premier en termes de performances? Une autre approche que je peux utiliser?

18
Ankit Vijay

Un problème que je vois en utilisant Task.WhenAll est qu'il ne renvoie pas de résultats

Mais cela fait retourne les résultats. Ils seront tous dans un tableau d'un type commun, il n'est donc pas toujours tile d'utiliser les résultats dans la mesure où vous devez trouver l'élément dans le tableau qui correspond au Task pour lequel vous souhaitez obtenir le résultat et éventuellement le convertir en son type réel, ce n'est donc peut-être pas l'approche la plus simple/la plus lisible dans ce contexte, mais lorsque vous souhaitez simplement obtenir tous les résultats de chaque tâche et le type commun est le type que vous souhaitez traiter, alors c'est génial.

Pour récupérer le résultat, puis-je appeler task.Result après avoir appelé Task.WhenAll, car toutes les tâches sont déjà terminées et tout ce dont j'ai besoin pour nous récupérer la réponse?

Oui, tu peux le faire. Vous pouvez également await eux (await déballerait l'exception dans toute tâche défectueuse, tandis que Result lèverait une exception agrégée, mais sinon ce serait la même).

Ce code est-il meilleur que le premier en termes de performances?

Il effectue les deux opérations en même temps, plutôt que l'une puis l'autre. Que ce soit meilleur ou pire dépend de ce que sont ces opérations sous-jacentes. Si les opérations sous-jacentes sont "lire un fichier à partir du disque", alors les faire en parallèle est probablement plus lent, car il n'y a qu'une seule tête de disque et elle ne peut être à un endroit à un moment donné; passer d'un fichier à l'autre sera plus lent que de lire un fichier puis un autre. D'un autre côté, si les opérations "effectuent une requête réseau" (comme c'est le cas ici), elles seront très probablement plus rapides (au moins jusqu'à un certain nombre de requêtes simultanées), car vous pouvez attendre une réponse à partir d'un autre ordinateur réseau tout aussi rapidement lorsqu'il y a également une autre demande réseau en attente. Si vous voulez savoir si c'est plus rapide dans votre situation, testez-le.

Une autre approche que je peux utiliser?

S'il n'est pas important pour vous que vous sachiez toutes des exceptions levées parmi toutes les opérations que vous faites en parallèle plutôt que juste la première, vous pouvez simplement await les tâches sans WhenAll du tout. La seule chose que WhenAll vous donne est d'avoir un AggregateException avec chaque exception unique de chaque tâche défaillante, plutôt que de lancer lorsque vous frappez la première tâche défaillante. C'est aussi simple que:

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

var result1 = await task1;
var result2 = await task2;
17
Servy

Voici la méthode d'extension qui utilise SemaphoreSlim et permet de définir le degré maximum de parallélisme

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

Exemple d'utilisation:

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