Jak mogę używać Async z ForEach?

123

Czy można używać Async podczas korzystania z ForEach? Poniżej kod, który próbuję:

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

Otrzymuję błąd:

Nazwa „Async” nie istnieje w obecnym kontekście

Metoda, w której zawarta jest instrukcja using, jest ustawiona na asynchroniczną.

James Jeffery
źródło

Odpowiedzi:

180

List<T>.ForEachnie działa szczególnie dobrze z async(również LINQ-to-objects, z tych samych powodów).

W takim przypadku zalecam rzutowanie każdego elementu na operację asynchroniczną, a następnie możesz (asynchronicznie) poczekać, aż wszystkie się zakończą.

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

Zalety tego podejścia w porównaniu z asyncdelegowaniem delegata ForEachto:

  1. Obsługa błędów jest bardziej odpowiednia. Wyjątki od async voidnie można złapać catch; to podejście spowoduje propagację wyjątków w await Task.WhenAlllinii, umożliwiając naturalną obsługę wyjątków.
  2. Wiesz, że na końcu tej metody zadania są zakończone, ponieważ wykonuje ona plik await Task.WhenAll. Jeśli używasz async void, nie możesz łatwo stwierdzić, kiedy operacje zostały zakończone.
  3. Takie podejście ma naturalną składnię do pobierania wyników. GetAdminsFromGroupAsyncwygląda na to, że jest to operacja, która daje wynik (administratorzy), a taki kod jest bardziej naturalny, jeśli takie operacje mogą zwracać wyniki, zamiast ustawiać wartość jako efekt uboczny.
Stephen Cleary
źródło
5
Nie zmienia to niczego, ale List.ForEach()nie jest częścią LINQ.
svick,
Świetna sugestia @StephenCleary i dziękuję za wszystkie udzielone odpowiedzi async. Byli bardzo pomocni!
Justin Helgerson,
4
@StewartAnderson: zadania będą wykonywane jednocześnie. Nie ma rozszerzenia do wykonywania seryjnego; po prostu zrób foreachz awaitin your loop body.
Stephen Cleary
1
@mare: ForEachprzyjmuje tylko typ delegata synchronicznego i nie ma przeciążenia przyjmującego typ delegata asynchronicznego. Więc krótka odpowiedź brzmi: „nikt nie napisał asynchronicznego ForEach”. Dłuższą odpowiedzią jest to, że musiałbyś przyjąć pewną semantykę; np. czy elementy powinny być przetwarzane pojedynczo (jak foreach), czy jednocześnie (jak Select)? Jeśli pojedynczo, czy strumienie asynchroniczne nie byłyby lepszym rozwiązaniem? Jeśli jednocześnie, czy wyniki powinny być w oryginalnej kolejności, czy w kolejności ukończenia? Czy powinno zawieść przy pierwszym niepowodzeniu, czy też poczekać, aż wszystko się zakończy? Itd.
Stephen Cleary
2
@RogerWolf: Tak; służy SemaphoreSlimdo ograniczania zadań asynchronicznych.
Stephen Cleary
61

Ta mała metoda rozszerzenia powinna zapewnić bezpieczną dla wyjątków iterację asynchroniczną:

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

Ponieważ zmieniamy zwracany typ lambda z voidna Task, wyjątki będą propagowane poprawnie. To pozwoli ci napisać coś takiego w praktyce:

await db.Groups.ToList().ForEachAsync(async i => {
    await GetAdminsFromGroup(i.Gid);
});
JD Courtoy
źródło
Myślę, że asyncpowinno być wcześnieji =>
Todd
Zamiast czekać na ForEachAsyn (), można również wywołać Wait ().
Jonas
Nie trzeba tu czekać na lambdę.
hazzik
Dodałbym obsługę CancellationToken do tego, jak w odpowiedzi Todda tutaj stackoverflow.com/questions/29787098/ ...
Zorkind
Zasadniczo ForEachAsyncjest to metoda biblioteczna, więc oczekiwanie powinno być prawdopodobnie skonfigurowane z ConfigureAwait(false).
Theodor Zoulias
9

Prostą odpowiedzią jest użycie foreachsłowa kluczowego zamiast ForEach()metody List().

using (DataContext db = new DataLayer.DataContext())
{
    foreach(var i in db.Groups)
    {
        await GetAdminsFromGroup(i.Gid);
    }
}
Gumowa kaczuszka
źródło
Jesteś geniuszem
Vick_onrails
8

Oto rzeczywista działająca wersja powyższych wariantów asynchronicznych dla wszystkich z przetwarzaniem sekwencyjnym:

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

Oto realizacja:

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

Jaka jest kluczowa różnica? .ConfigureAwait(false);który zachowuje kontekst głównego wątku podczas asynchronicznego sekwencyjnego przetwarzania każdego zadania.

mrogunlana
źródło
6

Począwszy od programu C# 8.0, możesz tworzyć i wykorzystywać strumienie asynchronicznie.

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

Więcej

Andrei Krasutski
źródło
1
Ma to tę zaletę, że oprócz oczekiwania na każdy element, teraz również oczekuje się MoveNextna moduł wyliczający. Jest to ważne w przypadkach, gdy moduł wyliczający nie może natychmiast pobrać następnego elementu i musi czekać, aż jeden stanie się dostępny.
Theodor Zoulias
3

Dodaj tę metodę rozszerzenia

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

A następnie użyj w ten sposób:

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();
superlogiczny
źródło
2

Problem polegał na tym, że asyncsłowo kluczowe musiało występować przed lambdą, a nie przed ciałem:

db.Groups.ToList().ForEach(async (i) => {
    await GetAdminsFromGroup(i.Gid);
});
James Jeffery
źródło
35
-1 za niepotrzebne i subtelne użycie async void. To podejście ma problemy z obsługą wyjątków i wiedzą, kiedy operacje asynchroniczne zostaną ukończone.
Stephen Cleary,
Tak, stwierdziłem, że to nie obsługuje poprawnie wyjątków.
Herman Schoenfeld