Nasza strona używa cookies. Dowiedz się więcej o celu ich używania i zmianie ustawień w przeglądarce. Korzystając ze strony, wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki. Rozumiem

Iteracja asynchroniczna: Jak korzystać z async i await z foreach w C#

Toby Mason-Barney Software Developer / The Football Pools
Sprawdź, jak przyspieszyć iterację w C#, używając asynchronicznych tasków w pętli foreach.
Iteracja asynchroniczna: Jak korzystać z async i await z foreach w C#

W tym poście przyjrzymy się, jak można iterować używając pętli foreach asynchronicznie. Pewnie zastanawiasz się, dlaczego miałbym używać czegoś takiego, skoro mogę po prostu zrobić coś w tym stylu…

  
//Asynchroniczna metoda do wywołania
public static Task DoAsync(string Item)
{
    Task.Delay(1000);
    Console.WriteLine($"Item: {Item}");
    return Task.CompletedTask;
}

//Metoda wykorzystująca pętle
public  static async Task BadLoopAsync(IEnumerable<string> thingsToLoop)
{
    foreach (var thing in thingsToLoop)
    {
        await DoAsync(thing);
    }        
}


To nie jest najlepsze podejście, mimo iż działa. Siedzenie w pętli synchronicznej, czekając, aż każdy Task zostanie ukończony jeden po drugim, zajmie sporo czasu. Jeżeli faktycznie każdy Task jest zależny od poprzedniego, to owszem, należy je wykonywać jeden po drugim. Ale jeśli nie są zależne, tylko marnujemy czas.


Taski i obietnice, które składają

Aby zrozumieć, dlaczego powyższy kod jest zły, dobrze jest zrozumieć Taski i sposób ich działania.

Dokładne wyjaśnienie Taska wykracza poza zakres tego artykułu. Dołączam link do porównania z JavaScriptem, ponieważ uważam, że implementacja mechanizmów asynchronicznych jest tam zrobiona w taki sposób, że naprawdę pomaga zrozumieć temat.

Postaram się krótko omówić, czym jest Task. W uproszczeniu, jest to zadanie, które musi zostać wykonane. Task zwrócony z metody asynchronicznej w istocie mówi: Hej, zadanie jest wykonywane, jeszcze nie jest zrobione, więc tutaj masz Task, który reprezentuje to zadanie, a po jego zakończeniu wrócimy tutaj i będziemy kontynuować.

W efekcie obiecuje, że zostanie wykonana pewna praca i że po jej zakończeniu wrócimy tutaj, aby kontynuować.


Asynchroniczne wykonywanie pętli

Tak więc dotarliśmy wreszcie do etapu, który ten artykuł ma adresować. Podczas asynchronicznej iteracji należy wziąć pod uwagę dwie rzeczy: czy zwracamy z powrotem wartość, czy też nie (void).

Zwracanie Void

Najpierw przyjrzymy się zwracaniu void i jego różnym częściom.

//Asynchroniczna metoda na którą będzie się czekać
public static Task DoAsync(string Item)
{
    Task.Delay(1000);
    Console.WriteLine($"Item: {Item}");
    return Task.CompletedTask;
}

//Metoda do przeiterowania po kolekcji i metoda await DoAsync
public static async Task LoopAsync(IEnumerable<string> thingsToLoop)
{
    List<Task> listOfTasks = new List<Task>();

    foreach (var thing in thingsToLoop)
    {
        listOfTasks.Add(DoAsync(thing));
    }

    await Task.WhenAll(listOfTasks);
}


Powyższy kod jest bardzo podobny do fragmentu kodu na początku artykułu, różnica polega na tym, że słowo kluczowe await jest używane w inny sposób. Gdy metoda jest wywoływana, pierwszą rzeczą, którą musimy zrobić, jest utworzenie kolekcji zadań (ponieważ nasza metoda zwraca Task, co jest asynchronicznym sposobem na powiedzenie "zwraca void"). Tutaj tworzymy List<Task>, ale można też użyć innych typów kolekcji. Kiedy już to zrobimy, możemy zacząć iterować po naszych Enumerable thingsToLoop, który został przekazany do metody.

Następnie kolekcja listOfTasks wchodzi do gry. Zamiast wywoływać metodę DoAsync i czekać na nią wprost, wywołujemy ją i dodajemy zwrócony przez nią obiekt Task do naszej kolekcji. W myśl poprzedniej sekcji o Taskach, jest to obietnica wykonania pracy.

Po dodaniu wszystkich zadań do naszej listy, możemy użyć statycznej metody w obiekcie Task, zwanej WhenAll. Ta metoda jest używana, gdy masz kilka zadań, które naraz chcesz potraktować awaitem. Następnie używamy await, by poczekać, aż zakończy się wykonywanie wszystkich zadań w naszej kolekcji. Po zakończeniu metoda zwraca swój Task jako wykonany do miejsca, z którego została wywołana i tym samym kończy się nasz program.

Rozwiązuje to nasz pierwotny problem w pierwszym fragmencie kodu. Nie jesteśmy już w sytuacji, w której pętla oczekuje na każdy Task po kolei. Oczekujemy teraz na zakończenie wszystkich Tasków, zanim zwrócimy ukończony Task do funkcji wywołującej.

Tym samym, wszystkie procesy wymagane do kontynuacji pracy są ukończone.

Zwracanie wartości

Teraz zobaczmy, jak zrobić to samo, ale zwrócić przy okazji kolekcję wartości po zakończeniu Taska.

//Asynchroniczna metoda, na którą będzie się czekać
public static Task<string> DoAsyncResult(string item)
{
    Task.Delay(1000);
    return Task.FromResult(item);
}

//Metoda do przeiterowania po kolekcji i metoda await DoAsyncResult
public static async Task<IEnumerable<string>> LoopAsyncResult(IEnumerable<string> thingsToLoop) 
{
    List<Task<string>> listOfTasks = new List<Task<string>>();

    foreach (var thing in thingsToLoop)
    {
        listOfTasks.Add(DoAsyncResult(thing));
    }

    return await Task.WhenAll<string>(listOfTasks);
}


Jak widać, kod jest bardzo podobny do przykładu, który nie zwraca wartości. Nadal tworzymy naszą listę Tasków i dodajemy nasze zwrócone Taski z wywołanej metody DoAsyncResult.

Różnica w tym przykładzie to zwracane typy i typy Tasków. Zamiast używać Taska, który jest asynchroniczną wersją voida (możesz zwrócić void, ale nie jest to dobra praktyka), zwracamy Task<T>, który jest ogólną implementacją Taska. Task<T> pozwala określić typ, który zostanie zwrócony po zakończeniu zadania, w naszym przykładzie jest to ciąg znaków.

Nasz DoAsyncResult zwraca ciąg znaków, więc gdy Taski dodane do naszej kolekcji zakończą się, zwrócą string. Metoda WhenAll, również posiada ogólną implementację, która pozwala na ustawienie zwracanych typów i jak widać, ustawiliśmy to na string. Typ zwracany metody WhenAll<T> jest IEnumerable określonego typu, który możemy łatwo zwrócić.


C# 8 na ratunek

Przyszłość tego problemu jest ciekawa, gdyż rozwiązanie jest w drodze. C# 8 - następna oficjalna wersja języka C# - będzie zawierać strumienie asynchroniczne. Korzystając z tej nowej funkcji, będziesz mógł zastosować słowo kluczowe await bezpośrednio do pętli foreach!

await foreach (var name in GetNamesAsync())


Powyższe to przykład na to, jak to będzie wyglądać. Nie korzystałem jeszcze z tej lub jakiejkolwiek funkcji C# 8, ale wzbogaci się on o zestaw ciekawych funkcji (możesz dowiedzieć się więcej tutaj).


Pomocne rozszerzenia

Dodałem kilka pomocnych metod do implementacji powyższego przykładu. Rozszerzają interfejs IEnumerable i są szybkim sposobem uruchamiania pewnych metod asynchronicznych na kolekcji przekazanych elementów.

public static Task LoopAsync<T>(this IEnumerable<T> list, Func<T, Task> function)
{
    return Task.WhenAll(list.Select(function));
}

public async static Task<IEnumerable<S>> LoopAsyncResult<TIn, TOut>(this IEnumerable<TIn> list, Func<TIn, Task<TOut>> function)
{
    var loopResult = await Task.WhenAll(list.Select(function));

    return loopResult.ToList().AsEnumerable();
}


Mogą się okazać przydatne lub nie. Dopasowałem je do konkretnego kontekstu, w którym je napisałem, więc jeśli chciałbyś je skopiować i dostosować do siebie, to się nie krępuj.


Wnioski

Proszę bardzo, oto sposób na uniknięcie problemu wywoływania metod asynchronicznych w pętli foreach. Ten artykuł został napisany głównie w celu pokazania, jak go szybko rozwiązać, bez zagłębiania się w przyczynę i działanie problemu.

Mam nadzieję, że pomogłem. Na razie, miłego kodowania!

Masz coś do powiedzenia?

Podziel się tym z 120 tysiącami naszych czytelników

Dowiedz się więcej
Rocket dog