web-dev-qa-db-fra.com

Comment utiliser Async avec ForEach?

Est-il possible d'utiliser Async en utilisant ForEach? Voici le code que j'essaye:

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

Je reçois 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.

90
James Jeffery

List<T>.ForEach ne joue pas très bien avec async (ni LINQ-to-objets, pour les mêmes raisons).

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

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 à l'attribution d'un délégué async à ForEach sont les suivants:

  1. La gestion des erreurs est plus appropriée. Exceptions de async void ne peut pas être attrapé avec catch; cette approche propagera des exceptions au await Task.WhenAll _, permettant la gestion naturelle des exceptions.
  2. Vous savez que les tâches sont terminées à la fin de cette méthode car elle effectue un await Task.WhenAll. Si tu utilises async void, vous ne pouvez pas facilement savoir quand les opérations sont terminées.
  3. Cette approche a une syntaxe naturelle pour récupérer les résultats. GetAdminsFromGroupAsync ressemble à une opération qui produit un résultat (les administrateurs), et ce code est plus naturel si de telles opérations peuvent retourner leurs résultats plutôt que de définir une valeur en tant qu'effet secondaire.
144
Stephen Cleary

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

Comme nous modifions le type de retour du lambda de void à Task, les exceptions se propagent correctement. Cela vous permettra d’écrire quelque chose comme ceci dans la pratique:

await db.Groups.ToList().ForEachAsync(async i => {
    await GetAdminsFromGroup(i.Gid);
});
44
JD Courtoy

Voici une version de travail de la variante asynchrone ci-dessus pour chaque variante 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 l'implémentation:

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 différence clé? .ConfigureAwait(false); qui conserve le contexte du thread principal lors du traitement séquentiel asynchrone de chaque tâche.

7
mrogunlana

Le problème était que le mot clé async devait apparaître avant le lambda, pas avant le corps:

db.Groups.ToList().ForEach(async (i) => {
    await GetAdminsFromGroup(i.Gid);
});
3
James Jeffery

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 si:

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();
3
superlogical

La réponse simple consiste à utiliser le mot clé foreach à la place de la méthode ForEach() de List().

using (DataContext db = new DataLayer.DataContext())
{
    foreach(var i in db.Groups)
    {
        await GetAdminsFromGroup(i.Gid);
    }
}
2
RubberDuck