Come posso usare Async con ForEach?


123

È possibile utilizzare Async quando si utilizza ForEach? Di seguito è riportato il codice che sto provando:

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

Ricevo l'errore:

Il nome "Async" non esiste nel contesto corrente

Il metodo in cui è racchiusa l'istruzione using è impostato su async.

Risposte:


180

List<T>.ForEachnon funziona particolarmente bene con async(né LINQ-to-objects, per gli stessi motivi).

In questo caso, ti consiglio di proiettare ogni elemento in un'operazione asincrona e puoi quindi (in modo asincrono) attendere che vengano completati tutti.

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

I vantaggi di questo approccio rispetto all'assegnazione di un asyncdelegato a ForEachsono:

  1. La gestione degli errori è più corretta. Le eccezioni da async voidnon possono essere catturate con catch; questo approccio propagherà le eccezioni alla await Task.WhenAlllinea, consentendo la gestione naturale delle eccezioni.
  2. Sai che le attività sono complete alla fine di questo metodo, poiché esegue un file await Task.WhenAll. Se usi async void, non puoi dire facilmente quando le operazioni sono state completate.
  3. Questo approccio ha una sintassi naturale per il recupero dei risultati. GetAdminsFromGroupAsyncsembra che sia un'operazione che produce un risultato (gli amministratori), e tale codice è più naturale se tali operazioni possono restituire i loro risultati piuttosto che impostare un valore come effetto collaterale.

5
Non che cambi nulla, ma List.ForEach()non fa parte di LINQ.
svick

Ottimo suggerimento @StephenCleary e grazie per tutte le risposte che hai fornito async. Sono stati molto utili!
Justin Helgerson

4
@StewartAnderson: le attività verranno eseguite contemporaneamente. Non c'è estensione per l'esecuzione seriale; basta fare un foreachcon un awaitcorpo nel tuo loop.
Stephen Cleary

1
@mare: ForEachaccetta solo un tipo di delegato sincrono e non è presente alcun sovraccarico che accetta un tipo di delegato asincrono. Quindi la risposta breve è "nessuno ha scritto un asincrono ForEach". La risposta più lunga è che dovresti assumere un po 'di semantica; ad esempio, gli articoli dovrebbero essere elaborati uno alla volta (come foreach) o simultaneamente (come Select)? Se uno alla volta, i flussi asincroni non sarebbero una soluzione migliore? Se contemporaneamente, i risultati dovrebbero essere nell'ordine dell'articolo originale o in ordine di completamento? Dovrebbe fallire al primo guasto o attendere che tutto sia completato? Ecc.
Stephen Cleary

2
@RogerWolf: Sì; utilizzare SemaphoreSlimper limitare le attività asincrone.
Stephen Cleary

61

Questo piccolo metodo di estensione dovrebbe darti un'iterazione asincrona sicura rispetto alle eccezioni:

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

Poiché stiamo cambiando il tipo di ritorno del lambda da voida Task, le eccezioni si propagheranno correttamente. Questo ti permetterà di scrivere qualcosa di simile in pratica:

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

Credo che asyncdovrebbe essere primai =>
Todd

Invece di attendere ForEachAsyn (), si potrebbe anche chiamare Wait ().
Jonas

Lambda non ha bisogno di essere atteso qui.
hazzik

Vorrei aggiungere il supporto per CancellationToken in quella come in risposta di Todd qui stackoverflow.com/questions/29787098/...
Zorkind

Il ForEachAsyncè essenzialmente un metodo di raccolta, in modo che il attesa dovrebbe essere probabilmente configurato con ConfigureAwait(false).
Theodor Zoulias

9

La semplice risposta è usare la foreachparola chiave invece del ForEach()metodo di List().

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

Sei un genio
Vick_onrails

8

Ecco una versione funzionante effettiva delle varianti foreach asincrone di cui sopra con elaborazione sequenziale:

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

Ecco l'implementazione:

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

Qual è la differenza fondamentale? .ConfigureAwait(false);che mantiene il contesto del thread principale durante l'elaborazione sequenziale asincrona di ciascuna attività.


6

A partire da C# 8.0, puoi creare e consumare flussi in modo asincrono.

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

Di Più


1
Questo ha il vantaggio che oltre ad aspettare ogni elemento, ora stai anche aspettando il MoveNextdel enumeratore. Ciò è importante nei casi in cui l'enumeratore non può recuperare immediatamente l'elemento successivo e deve attendere che uno diventi disponibile.
Theodor Zoulias

3

Aggiungi questo metodo di estensione

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

E poi usa in questo modo:

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

2

Il problema era che la asyncparola chiave doveva apparire prima del lambda, non prima del corpo:

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

35
-1 per un uso non necessario e sottile di async void. Questo approccio presenta problemi relativi alla gestione delle eccezioni e alla conoscenza del completamento delle operazioni asincrone.
Stephen Cleary

Sì, ho scoperto che questo non gestisce correttamente le eccezioni.
Herman Schoenfeld,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.