Comment puis-je utiliser Async avec ForEach?

123

Est-il possible d'utiliser Async lors de l'utilisation de ForEach? Voici le code que j'essaye:

using (DataContext db = new DataLayer.DataContext())
{
    db.Groups.ToList().ForEach(i => async {
        await GetAdminsFromGroup(i.Gid);
    });
}

J'obtiens l'erreur:

Le nom 'Async' n'existe pas dans le contexte actuel

La méthode dans laquelle l'instruction using est incluse est définie sur async.

James Jeffery
la source

Réponses:

180

List<T>.ForEachne joue pas particulièrement bien avec async(LINQ-to-objects non plus, pour les mêmes raisons).

Dans ce cas, je recommande de projeter chaque élément dans une opération asynchrone, et vous pouvez ensuite (de manière asynchrone) attendre qu'ils se terminent tous.

using (DataContext db = new DataLayer.DataContext())
{
    var tasks = db.Groups.ToList().Select(i => GetAdminsFromGroupAsync(i.Gid));
    var results = await Task.WhenAll(tasks);
}

Les avantages de cette approche par rapport au fait de donner à un asyncdélégué ForEachsont les suivants:

  1. La gestion des erreurs est plus appropriée. Les exceptions à async voidne peuvent pas être prises avec catch; cette approche propagera les exceptions à la await Task.WhenAllligne, permettant une gestion naturelle des exceptions.
  2. Vous savez que les tâches sont terminées à la fin de cette méthode, puisqu'elle fait un await Task.WhenAll. Si vous utilisez async void, vous ne pouvez pas facilement dire quand les opérations sont terminées.
  3. Cette approche a une syntaxe naturelle pour récupérer les résultats. GetAdminsFromGroupAsyncressemble à une opération qui produit un résultat (les administrateurs), et un tel code est plus naturel si de telles opérations peuvent renvoyer leurs résultats plutôt que de définir une valeur comme effet secondaire.
Stephen Cleary
la source
5
Non pas que cela change quoi que ce soit, mais List.ForEach()ne fait pas partie de LINQ.
svick
Excellente suggestion @StephenCleary et merci pour toutes les réponses que vous avez données async. Ils ont été très utiles!
Justin Helgerson
4
@StewartAnderson: les tâches seront exécutées simultanément. Il n'y a pas d'extension pour l'exécution en série; faites simplement un foreachavec un awaitcorps dans votre boucle.
Stephen Cleary
1
@mare: ForEachprend uniquement un type de délégué synchrone, et il n'y a pas de surcharge prenant un type de délégué asynchrone. Donc, la réponse courte est "personne n'a écrit un asynchrone ForEach". La réponse la plus longue est que vous devez assumer une certaine sémantique; Par exemple, les éléments doivent-ils être traités un à la fois (comme foreach) ou simultanément (comme Select)? Si un à la fois, les flux asynchrones ne seraient-ils pas une meilleure solution? Si simultanément, les résultats doivent-ils être dans l'ordre original de l'article ou dans l'ordre d'achèvement? Doit-il échouer au premier échec ou attendre que tout soit terminé? Etc.
Stephen Cleary
2
@RogerWolf: Oui; utiliser SemaphoreSlimpour limiter les tâches asynchrones.
Stephen Cleary
61

Cette petite méthode d'extension devrait vous donner une itération asynchrone sans exception:

public static async Task ForEachAsync<T>(this List<T> list, Func<T, Task> func)
{
    foreach (var value in list)
    {
        await func(value);
    }
}

Puisque nous changeons le type de retour du lambda de voidà Task, les exceptions se propageront correctement. Cela vous permettra d'écrire quelque chose comme ça dans la pratique:

await db.Groups.ToList().ForEachAsync(async i => {
    await GetAdminsFromGroup(i.Gid);
});
JD Courtoy
la source
Je crois que asyncdevrait être avanti =>
Todd
Au lieu d'attendre ForEachAsyn (), on pourrait également appeler un Wait ().
Jonas
Lambda n'a pas besoin d'être attendu ici.
hazzik
J'ajouterais le support pour CancellationToken dans cela comme dans la réponse de Todd ici stackoverflow.com/questions/29787098/…
Zorkind
Il ForEachAsyncs'agit essentiellement d'une méthode de bibliothèque, donc l'attente devrait probablement être configurée avec ConfigureAwait(false).
Theodor Zoulias
9

La réponse simple est d'utiliser le foreachmot - clé au lieu de la ForEach()méthode de List().

using (DataContext db = new DataLayer.DataContext())
{
    foreach(var i in db.Groups)
    {
        await GetAdminsFromGroup(i.Gid);
    }
}
Canard en caoutchouc
la source
Vous êtes un génie
Vick_onrails
8

Voici une version de travail réelle des variantes async foreach ci-dessus avec traitement séquentiel:

public static async Task ForEachAsync<T>(this List<T> enumerable, Action<T> action)
{
    foreach (var item in enumerable)
        await Task.Run(() => { action(item); }).ConfigureAwait(false);
}

Voici la mise en œuvre:

public async void SequentialAsync()
{
    var list = new List<Action>();

    Action action1 = () => {
        //do stuff 1
    };

    Action action2 = () => {
        //do stuff 2
    };

    list.Add(action1);
    list.Add(action2);

    await list.ForEachAsync();
}

Quelle est la principale différence? .ConfigureAwait(false);qui conserve le contexte du thread principal pendant le traitement séquentiel asynchrone de chaque tâche.

mrogunlana
la source
6

En commençant par C# 8.0, vous pouvez créer et consommer des flux de manière asynchrone.

    private async void button1_Click(object sender, EventArgs e)
    {
        IAsyncEnumerable<int> enumerable = GenerateSequence();

        await foreach (var i in enumerable)
        {
            Debug.WriteLine(i);
        }
    }

    public static async IAsyncEnumerable<int> GenerateSequence()
    {
        for (int i = 0; i < 20; i++)
        {
            await Task.Delay(100);
            yield return i;
        }
    }

Plus

Andrei Krasutski
la source
1
Cela présente l'avantage qu'en plus d'attendre chaque élément, vous attendez maintenant également le MoveNextde l'énumérateur. Ceci est important dans les cas où l'énumérateur ne peut pas récupérer l'élément suivant instantanément et doit attendre qu'il soit disponible.
Theodor Zoulias
3

Ajouter cette méthode d'extension

public static class ForEachAsyncExtension
{
    public static Task ForEachAsync<T>(this IEnumerable<T> source, int dop, Func<T, Task> body)
    {
        return Task.WhenAll(from partition in Partitioner.Create(source).GetPartitions(dop) 
            select Task.Run(async delegate
            {
                using (partition)
                    while (partition.MoveNext())
                        await body(partition.Current).ConfigureAwait(false);
            }));
    }
}

Et puis utilisez comme ceci:

Task.Run(async () =>
{
    var s3 = new AmazonS3Client(Config.Instance.Aws.Credentials, Config.Instance.Aws.RegionEndpoint);
    var buckets = await s3.ListBucketsAsync();

    foreach (var s3Bucket in buckets.Buckets)
    {
        if (s3Bucket.BucketName.StartsWith("mybucket-"))
        {
            log.Information("Bucket => {BucketName}", s3Bucket.BucketName);

            ListObjectsResponse objects;
            try
            {
                objects = await s3.ListObjectsAsync(s3Bucket.BucketName);
            }
            catch
            {
                log.Error("Error getting objects. Bucket => {BucketName}", s3Bucket.BucketName);
                continue;
            }

            // ForEachAsync (4 is how many tasks you want to run in parallel)
            await objects.S3Objects.ForEachAsync(4, async s3Object =>
            {
                try
                {
                    log.Information("Bucket => {BucketName} => {Key}", s3Bucket.BucketName, s3Object.Key);
                    await s3.DeleteObjectAsync(s3Bucket.BucketName, s3Object.Key);
                }
                catch
                {
                    log.Error("Error deleting bucket {BucketName} object {Key}", s3Bucket.BucketName, s3Object.Key);
                }
            });

            try
            {
                await s3.DeleteBucketAsync(s3Bucket.BucketName);
            }
            catch
            {
                log.Error("Error deleting bucket {BucketName}", s3Bucket.BucketName);
            }
        }
    }
}).Wait();
superlogique
la source
2

Le problème était que le asyncmot - clé doit apparaître avant le lambda, pas avant le corps:

db.Groups.ToList().ForEach(async (i) => {
    await GetAdminsFromGroup(i.Gid);
});
James Jeffery
la source
35
-1 pour une utilisation inutile et subtile de async void. Cette approche a des problèmes autour de la gestion des exceptions et de savoir quand les opérations asynchrones se terminent.
Stephen Cleary
Oui, j'ai trouvé que cela ne gère pas correctement les exceptions.
Herman Schoenfeld