Appel de plusieurs services asynchrones en parallèle

17

J'ai peu de services REST asynchrones qui ne dépendent pas les uns des autres. C'est 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, service2Responsene dépend pas service1Responseet 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 l'utiliser Parallel.ForEachici car ce n'est pas une opération liée au CPU.

Pour appeler ces deux opérations en parallèle, puis-je appeler use Task.WhenAll? Un problème que je vois utiliser Task.WhenAllest qu'il ne renvoie pas de résultats. Pour récupérer le résultat, puis-je appeler task.Resultaprè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?

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?

Ankit Vijay
la source
I do not think I can use Parallel.ForEach here since it is not CPU bound operation- Je ne vois pas la logique là-bas. La concurrence est la concurrence.
Robert Harvey
3
@RobertHarvey Je suppose que le problème est que, dans ce contexte, Parallel.ForEachengendrerait de nouveaux threads alors async awaitqu'il ferait tout sur un seul thread.
MetaFight
@Ankit, cela dépend quand il est approprié que votre code soit bloqué. Votre deuxième exemple bloquerait jusqu'à ce que les deux réponses soient prêtes. Votre premier exemple, vraisemblablement, ne bloquerait logiquement que lorsque le code tenterait d'utiliser la réponse ( await) avant qu'il ne soit prêt.
MetaFight
Il pourrait être plus facile de vous donner une réponse plus satisfaisante si vous fournissez un exemple moins abstrait du code consommant les deux réponses de service.
MetaFight
@MetaFight Dans mon deuxième exemple que je fais WhenAllavant de faire Resultavec l'idée qu'il termine toutes les tâches avant que .Result soit appelé. Depuis, Task.Result bloque le thread appelant, je suppose que si je l'appelle une fois les tâches terminées, il retournerait immédiatement le résultat. Je veux valider la compréhension.
Ankit Vijay

Réponses:

17

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

Mais il ne renvoie les résultats. Ils seront tous dans un tableau d'un type commun, il n'est donc pas toujours utile d'utiliser les résultats dans la mesure où vous devez trouver l'élément dans le tableau qui correspond à Taskcelui pour lequel vous voulez le résultat, et potentiellement le caster dans son type réel, donc ce n'est peut-être pas l'approche la plus facile / la plus lisible dans ce contexte, mais quand vous voulez juste avoir tous les résultats de chaque tâche, et le type commun est le type que vous voulez 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 awaitles awaitutiliser ( déballer l'exception dans toute tâche défectueuse, alors Resultque lancer une exception agrégée, mais sinon ce serait la même chose).

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 tu veux savoir si c'est

Une autre approche que je peux utiliser?

S'il n'est pas important pour vous que vous connaissiez toutes les exceptions levées parmi toutes les opérations que vous effectuez en parallèle plutôt que juste la première, vous pouvez simplement effectuer awaitles tâches sans rien WhenAlldu tout. La seule chose que WhenAllvous donne est d'avoir un AggregateExceptionavec chaque exception unique de chaque tâche défectueuse, plutôt que de lancer lorsque vous frappez la première tâche défectueuse. C'est aussi simple que:

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

var result1 = await task1;
var result2 = await task2;
Servy
la source
Il ne s'agit pas d'exécuter des tâches simultanément et encore moins en parallèle. Vous attendez que chaque tâche se termine dans un ordre séquentiel. Complètement bien si vous ne vous souciez pas du code performant.
Rick O'Shea
3
@ RickO'Shea Il démarre les opérations séquentiellement. Il démarrera la deuxième opération après avoir * démarré la première opération. Mais le démarrage de l'opération asynchrone doit être essentiellement instantané (si ce n'est pas le cas, ce n'est pas réellement asynchrone, et c'est un bogue dans cette méthode). Après avoir démarré l'un, puis l'autre, il ne se poursuivra qu'après la première fin, puis la seconde. Étant donné que rien n'attend que le premier se termine avant de démarrer le second, rien ne les empêche de s'exécuter simultanément (ce qui revient au même que de les exécuter en parallèle).
Servy
@Servy Je ne pense pas que ce soit vrai. J'ai ajouté la journalisation dans deux opérations asynchrones qui ont pris environ une seconde chacune (les deux font des appels http), puis les ai appelées comme vous l'avez suggéré, et bien sûr, task1 a commencé et s'est terminé, puis task2 a commencé et s'est terminé.
Matt Frear
@MattFrear Ensuite, la méthode n'était pas en fait asynchrone. C'était synchrone. Par définition , une méthode asynchrone va revenir tout de suite, plutôt que de revenir après la fin de l'opération.
Servy
@Servy par définition, l'attente signifie que vous attendez la fin de la tâche asynchrone avant d'exécuter la ligne suivante. N'est-ce pas?
Matt Frear
0

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);
Jay Shah
la source
-2

Vous pouvez soit utiliser

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

ou

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();
user1451111
la source
Pourquoi downvotes?
user1451111