Comment attendre une liste de tâches de manière asynchrone en utilisant LINQ?

87

J'ai une liste de tâches que j'ai créées comme ceci:

public async Task<IList<Foo>> GetFoosAndDoSomethingAsync()
{
    var foos = await GetFoosAsync();

    var tasks = foos.Select(async foo => await DoSomethingAsync(foo)).ToList();

    ...
}

En utilisant .ToList(), les tâches devraient toutes commencer. Maintenant, je veux attendre leur achèvement et renvoyer les résultats.

Cela fonctionne dans le ...bloc ci-dessus :

var list = new List<Foo>();
foreach (var task in tasks)
    list.Add(await task);
return list;

Il fait ce que je veux, mais cela semble plutôt maladroit. Je préfère de loin écrire quelque chose de plus simple comme celui-ci:

return tasks.Select(async task => await task).ToList();

... mais cela ne compile pas. Qu'est-ce que je rate? Ou n'est-il tout simplement pas possible d'exprimer les choses de cette façon?

Matt Johnson-Pint
la source
Avez-vous besoin de traiter en DoSomethingAsync(foo)série pour chaque toto, ou s'agit-il d'un candidat pour Parallel.ForEach <Foo> ?
mdisibio
1
@mdisibio - Parallel.ForEachbloque. Le modèle ici vient de la vidéo Asynchronous C # de Jon Skeet sur Pluralsight . Il s'exécute en parallèle sans blocage.
Matt Johnson-Pint
@mdisibio - Non. Ils fonctionnent en parallèle. Essayez-le . (De plus, il semble que je n'en ai pas besoin .ToList()si je vais simplement l'utiliser WhenAll.)
Matt Johnson-Pint
Point pris. Selon la façon dont DoSomethingAsyncest écrite, la liste peut être exécutée ou non en parallèle. J'ai pu écrire une méthode de test qui était et une version qui ne l'était pas, mais dans les deux cas, le comportement est dicté par la méthode elle-même, et non par le délégué qui crée la tâche. Désolé pour la confusion. Cependant, si DoSomethingAsycrevient Task<Foo>, alors le awaitdans le délégué n'est pas absolument nécessaire ... Je pense que c'était le point principal que j'allais essayer de faire.
mdisibio

Réponses:

136

LINQ ne fonctionne pas parfaitement avec le asynccode, mais vous pouvez le faire:

var tasks = foos.Select(DoSomethingAsync).ToList();
await Task.WhenAll(tasks);

Si vos tâches renvoient toutes le même type de valeur, vous pouvez même le faire:

var results = await Task.WhenAll(tasks);

ce qui est assez sympa. WhenAllrenvoie un tableau, donc je pense que votre méthode peut renvoyer les résultats directement:

return await Task.WhenAll(tasks);
Stephen Cleary
la source
11
Je voulais juste souligner que cela peut également fonctionner avecvar tasks = foos.Select(foo => DoSomethingAsync(foo)).ToList();
mdisibio
1
ou mêmevar tasks = foos.Select(DoSomethingAsync).ToList();
Todd Menier
3
quelle est la raison pour laquelle Linq ne fonctionne pas parfaitement avec le code asynchrone?
Ehsan Sajjad le
2
@EhsanSajjad: Parce que LINQ to Objects fonctionne de manière synchrone sur les objets en mémoire. Certaines choses limitées fonctionnent, comme Select. Mais la plupart n'aiment pas Where.
Stephen Cleary
4
@EhsanSajjad: Si l'opération est basée sur les E / S, vous pouvez l'utiliser asyncpour réduire les threads; s'il est lié au processeur et déjà sur un thread d'arrière-plan, alors asyncne fournirait aucun avantage.
Stephen Cleary
9

Pour développer la réponse de Stephen, j'ai créé la méthode d'extension suivante pour conserver le style fluide de LINQ. Vous pouvez alors faire

await someTasks.WhenAll()

namespace System.Linq
{
    public static class IEnumerableExtensions
    {
        public static Task<T[]> WhenAll<T>(this IEnumerable<Task<T>> source)
        {
            return Task.WhenAll(source);
        }
    }
}
Clément
la source
10
Personnellement, je nommerais votre méthode d'extensionToArrayAsync
torvin
4

Un problème avec Task.WhenAll est qu'il créerait un parallélisme. Dans la plupart des cas, cela peut être encore mieux, mais parfois vous voulez l'éviter. Par exemple, lire des données par lots à partir de la base de données et envoyer des données à un service Web distant. Vous ne voulez pas charger tous les lots dans la mémoire, mais appuyez sur la base de données une fois que le lot précédent a été traité. Donc, vous devez briser l'asynchronisme. Voici un exemple:

var events = Enumerable.Range(0, totalCount/ batchSize)
   .Select(x => x*batchSize)
   .Select(x => dbRepository.GetEventsBatch(x, batchSize).GetAwaiter().GetResult())
   .SelectMany(x => x);
foreach (var carEvent in events)
{
}

Remarque .GetAwaiter (). GetResult () le convertissant en synchronisation. DB ne serait touché paresseusement qu'une fois que batchSize des événements ont été traités.

Boris Lipschitz
la source
1

Utilisez Task.WaitAllou Task.WhenAllselon ce qui est approprié.

KG
la source
1
Cela ne fonctionne pas non plus. Task.WaitAllest bloquant, n'est pas attendu et ne fonctionnera pas avec un Task<T>.
Matt Johnson-Pint
@MattJohnson WhenAll?
LB
Oui. C'est ça! Je me sens stupide. Merci!
Matt Johnson-Pint
0

Task.WhenAll devrait faire l'affaire ici.

Ameen
la source