Menu Zamknij

Task.WhenAll vs Multiple awaits in foreach

Cześć,
pod ostatnim postem, użytkownik DD zwrócił mi słuszną uwagę, którą chciałbym tutaj rozwinąć. Otóż chodziło o to aby zamiast w każdej iteracji pętli, wykonywać await na handlerze, można je wszystkie odpalić za pomocą metody Task.WhenAll(). W tym wpisie chciałbym omówić różnicę między tymi dwoma podejściami. Postaram się opisać za i przeciw a także samemu sprawdzić, w praktyce co okazuję się szybsze i mniej zawodne.

Różnice

Metoda Task.WhenAll() przyjmuje jako argument tablicę zadań a następnie zwraca nam obiekt typu Task. Jego ukończenie następuje po wykonaniu ostatniego z przekazanych do niego wszystkich zadań. W przypadku wystąpienia wyjątku, zostaje on zapisany do AggregateException, a status Taska ustawiony na Faulted. Program nie poinformuje nas o błędzie, a try catch który zastosujemy złapie tylko pierwszy wyjątek z góry. Musimy pamiętać że chcąc dostać pełną listę błędów, które wystąpiły w różnych zadaniach jesteśmy zmuszeni zrobić to sami, na przykład w ten sposób:

Ważnym faktem, o którym trzeba pamiętać decydując się na skorzystanie z metody WhenAll jest brak możliwości ustalenia kolejności wykonywania zadań. Jako że metoda ta jest częścią Task Parallel Library, zadania które zostaną do niej przekazane wykonywane są w sposób równoległy. Warto się zastanowić, czy to, co chcemy wykonać powinno mieć swoją ustaloną kolejność, tak aby nie doprowadzić np. do deadlocka. Innym ograniczeniem tej funkcji są zwracane typy. Mamy do wyboru albo skorzystać z „podstawowej” jej wersji, która zwraca nam jedynie obiekt typu Task a w nim informację o tym czy wszystko się powiodło, lub też skorzystanie z jej generycznej postaci Task.WhenAll<T>(). Narzuca nam to swojego rodzaju ograniczenie do jednego zwracanego typu.

Decydując się na skorzystanie z await mamy pełną dowolność w zwracanym typie danych. Dzieje się tak dlatego że wywołujemy za pomocą niego tylko jedną interesującą nas metodę, a nie cały ich zestaw. Dodatkowo możemy zarządzać kolejnością wywoływań, co dość często jest bardzo porządane. Znaczącą różnicę również możemy zauważyć w przypadku wyjątków. Otóż każdy który wystąpi, a nie zostanie odpowiednio obsłużony, przerwie działanie programu. Warto więc pamiętać o tych różnicach.

Foreach, z czego skorzystać?

Z metody Task.WhenAll() powinniśmy korzystać za każdy razem, kiedy posiadamy zbiór zadań do wykonania, które spełniają wyżej opisane wymagania. Oczywiście możemy przy każdej iteracji pętli czekać na każde z nich za pomocą await ale będzie to bardzo nieefektywne. Żeby lepiej zobrazować co mam na myśli posłużę się przykładem:

Powyższy kod ma za zadanie dla przekazanej tablicy stringów odpowiadającym nazwom słowników, zwrócić ich zawartość z bazy danych. Możemy zrobić to w sposób jak powyżej, czyli za pomocą foreach, dla każdej wartości z tablicy dictionaryNames wywołać metodę GetUserDictionary i odpowiedni słownik dopisać do listy, którą następnie zwrócimy. Warto jednak tu zauważyć, tak jak wcześniej wspominałem, że przy każdym wywołaniu metody z repozytorium będziemy czekać na jej zrealizowanie. Aby sprawić że proces będzie bardziej efektywny możemy zrefaktoryzować nasz kod w następujący sposób:

Tworzymy więc listę tasks, do ktorej dodamy wywołanie metody GetUserDictionary dla każdej wartości z tablicy dictionaryNames. Następnie za pomocą Task.WhenAll równolegle wykonamy wszystkie zadanie z tej listy. Zdaję sobie sprawę że powyższa implementacja mogłaby być zawarta bezpośrednio w repozytorium, aczkolwiek potrzebowałem jakiegoś dobrego przykładu który będzie pokazywał to o czym piszę, a te dwa powyższe idealnie się do tego nadają 🙂

Sprawdziłem również które z tych dwóch podejść jest szybsze, w następujący sposób:

Dzięki Krzysztof za zwrócenie uwagi na to że poprzedni przykład nie był poprawny. Pozwoliłem sobie skorzystać z twojej pomocy i edytować go według twoich uwag 🙂

Wyniki prezentowały się następująco:

Jak widzimy dzięki WhenAll dwukrotnie przyśpieszyliśmy wykonanie naszych zadań.

Podsumowanie

Task.WhenAll() odpowiednio zastosowany na pewno da „kopa”, jeżeli chodzi o szybkość wykonywanych przez niego zadań w stosunku do wielu awaitów. Jak wszystko, nieodpowiednio zastosowany może zrodzić nam problemy z deadlockami, czy niespodziewanymi wyjątkami, których zapomnieliśmy sprawdzić. Dlatego tam gdzie możemy korzystajmy z metody WhenAll(), a robiąc to z głową na pewno tylko na tym skorzystamy 🙂

5 Komentarzy

  1. Marcin

    Czy definicja metody WhenAllTasks nie powinna wyglądać tak:

    public async Task WhenAllTasks(List tasks)
    {
    var totalTasks = Task.WhenAll(tasks);

    try {
    await totalTasks;
    }
    catch {
    throw totalTasks.Exception; // Throw AggregateException
    }
    }

    Wydaje mi się że w przypadku kodu:

    public async Task WhenAllTasks(List tasks)
    {
    var totalTasks = Task.WhenAll(tasks);
    await totalTasks;

    if (totalTasks.Exception != null)
    throw totalTasks.Exception;
    }

    Linia await totalTask już rzuci wyjątek i nigdy nie przejdziemy do instrukcji if.

  2. Krzysztof DevKR

    Porównanie czasów zrobiłeś nie prawidłowo, patrząc na wyniki WhenAll: 223 Multiple awaits: 634218, od razu widać że proporcje są za duże. Do metody Delay przekazujesz milisekundy, to proponuję zamiast ElapsedTicks wypisywać ElapsedMilliseconds będzie to czytelniejsze. Na szybko widać że Multiple awaits powinno być ponad 2 razy wolniejsze w porównaniu z WhenAll. Zatem gdzie jest błąd. W pierwszym przypadku brakuję await, czyli zamiast var allTasks = Task.WhenAll(tasks); powinno być await Task.WhenAll(tasks);. W drugim przypadku wywołanie w pętli foreach nie jest prawidłowe. Poniżej przykładowa logika do porównaniu przypadków.

    Przypadek (WhenAll)

    var tasks = new List();

    foreach (var value in new[] { 1, 1.5, 2 })
    {
    tasks.Add(Task.Delay((int)(value * 100)));
    }

    await Task.WhenAll(tasks);

    Przypadek (Multiple awaits)

    foreach (var value in new[] { 1, 1.5, 2 })
    {
    await Task.Delay((int)(value * 100));
    }

    Pozdrawiam

    • contend

      Dziękuję ci bardzo. Faktycznie, zostało to źle napisane. Już poprawiłem według twoich uwag i pozwoliłem sobię o tym wspomnieć w poście. Raz jeszcze, dzięki 🙂

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *