Wie kann ich Async mit ForEach verwenden?

122

Ist es möglich, Async bei Verwendung von ForEach zu verwenden? Unten ist der Code, den ich versuche:

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

Ich erhalte den Fehler:

Der Name 'Async' existiert im aktuellen Kontext nicht

Die Methode, in der die using-Anweisung enthalten ist, ist auf asynchron gesetzt.

James Jeffery
quelle

Antworten:

179

List<T>.ForEachspielt nicht besonders gut mit async(LINQ-to-Objects aus den gleichen Gründen auch nicht).

In diesem Fall empfehle ich, jedes Element in eine asynchrone Operation zu projizieren , und Sie können dann (asynchron) warten, bis alle abgeschlossen sind.

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

Die Vorteile dieses Ansatzes gegenüber der Übergabe eines asyncDelegierten ForEachsind:

  1. Die Fehlerbehandlung ist besser. Ausnahmen von async voidkönnen nicht erfasst werden catch; Dieser Ansatz verbreitet Ausnahmen an der await Task.WhenAllZeile und ermöglicht so die natürliche Ausnahmebehandlung.
  2. Sie wissen, dass die Aufgaben am Ende dieser Methode abgeschlossen sind, da sie eine ausführt await Task.WhenAll. Wenn Sie verwenden async void, können Sie nicht leicht feststellen, wann die Vorgänge abgeschlossen sind.
  3. Dieser Ansatz hat eine natürliche Syntax zum Abrufen der Ergebnisse. GetAdminsFromGroupAsyncklingt wie es ist eine Operation , die ein Ergebnis (die Admins), und ein solcher Code ist natürlicher , wenn solche Operationen erzeugt , kann das Rück ihre Ergebnisse eher als Wert als Nebeneffekt einstellen.
Stephen Cleary
quelle
5
Nicht, dass es irgendetwas ändert, aber List.ForEach()nicht Teil von LINQ ist.
Svick
Toller Vorschlag @StephenCleary und vielen Dank für alle Antworten, die Sie gegeben haben async. Sie waren sehr hilfreich!
Justin Helgerson
4
@StewartAnderson: Die Aufgaben werden gleichzeitig ausgeführt. Es gibt keine Erweiterung für die serielle Ausführung. Mach einfach ein foreachmit einem awaitin deinem Loop-Körper.
Stephen Cleary
1
@mare: Nimmt ForEachnur einen synchronen Delegatentyp und es gibt keine Überlastung für einen asynchronen Delegattyp . Die kurze Antwort lautet also "niemand hat eine asynchrone geschrieben ForEach". Die längere Antwort ist, dass Sie eine gewisse Semantik annehmen müssten; z. B. sollten die Artikel einzeln (wie foreach) oder gleichzeitig (wie Select) verarbeitet werden? Wären asynchrone Streams nicht eine bessere Lösung? Wenn gleichzeitig, sollten die Ergebnisse in der ursprünglichen Artikelreihenfolge oder in der Reihenfolge der Fertigstellung vorliegen? Sollte es beim ersten Fehler fehlschlagen oder warten, bis alle abgeschlossen sind? Usw.
Stephen Cleary
2
@ RogerWolf: Ja; Verwenden Sie SemaphoreSlimdiese Option, um asynchrone Aufgaben zu drosseln.
Stephen Cleary
61

Diese kleine Erweiterungsmethode sollte Ihnen eine ausnahmesichere asynchrone Iteration bieten:

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

Da wir den Rückgabetyp des Lambda von voidauf ändern Task, werden Ausnahmen korrekt weitergegeben. Auf diese Weise können Sie in der Praxis so etwas schreiben:

await db.Groups.ToList().ForEachAsync(async i => {
    await GetAdminsFromGroup(i.Gid);
});
JD Courtoy
quelle
Ich glaube, asyncsollte vorher seini =>
Todd
Anstatt auf ForEachAsyn () zu warten, könnte man auch Wait () aufrufen.
Jonas
Lambda muss hier nicht erwartet werden.
Hazzik
Ich würde Unterstützung für CancellationToken hinzufügen, wie in Todd's Antwort hier stackoverflow.com/questions/29787098/…
Zorkind
Das ForEachAsyncist im Wesentlichen eine Bibliotheksmethode, daher sollte das Warten wahrscheinlich mit konfiguriert werden ConfigureAwait(false).
Theodor Zoulias
9

Die einfache Antwort besteht darin, das foreachSchlüsselwort anstelle der ForEach()Methode von zu verwenden List().

using (DataContext db = new DataLayer.DataContext())
{
    foreach(var i in db.Groups)
    {
        await GetAdminsFromGroup(i.Gid);
    }
}
Badeente
quelle
Du bist ein Genie
Vick_onrails
8

Hier ist eine aktuelle Arbeitsversion der oben genannten asynchronen foreach-Varianten mit sequentieller Verarbeitung:

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

Hier ist die Implementierung:

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

Was ist der Hauptunterschied? .ConfigureAwait(false);Dadurch bleibt der Kontext des Hauptthreads erhalten, während die sequentielle Verarbeitung jeder Aufgabe asynchronisiert wird.

Mrogunlana
quelle
6

Zunächst C# 8.0können Sie Streams asynchron erstellen und verwenden.

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

Mehr

Andrei Krasutski
quelle
1
Dies hat den Vorteil, dass Sie neben dem Warten auf jedes Element jetzt auch auf MoveNextden Enumerator warten . Dies ist wichtig in Fällen, in denen der Enumerator das nächste Element nicht sofort abrufen kann und warten muss, bis eines verfügbar ist.
Theodor Zoulias
3

Fügen Sie diese Erweiterungsmethode hinzu

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

Und dann benutze so:

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();
superlogisch
quelle
2

Das Problem war, dass das asyncSchlüsselwort vor dem Lambda und nicht vor dem Body stehen muss:

db.Groups.ToList().ForEach(async (i) => {
    await GetAdminsFromGroup(i.Gid);
});
James Jeffery
quelle
35
-1 für unnötige und subtile Verwendung von async void. Dieser Ansatz hat Probleme mit der Ausnahmebehandlung und dem Wissen, wann die asynchronen Vorgänge abgeschlossen sind.
Stephen Cleary
Ja, ich habe festgestellt, dass dies Ausnahmen nicht richtig behandelt.
Herman Schönfeld