Kiedy używać Task.WaitAll, a kiedy Task.WhenAll w .NET

TPL (Task Parallel Library) to jedna z najciekawszych nowych funkcji dodanych w ostatnich wersjach platformy .NET. Metody Task.WaitAll i Task.WhenAll to dwie ważne i często używane metody w TPL.

Task.WaitAll blokuje bieżący wątek do momentu zakończenia wykonywania wszystkich innych zadań. Metoda Task.WhenAll służy do tworzenia zadania, które zakończy się wtedy i tylko wtedy, gdy wszystkie inne zadania zostaną ukończone.

Tak więc, jeśli używasz Task.WhenAll, otrzymasz obiekt zadania, który nie jest ukończony. Jednak nie będzie blokować, ale pozwoli na wykonanie programu. Wręcz przeciwnie, wywołanie metody Task.WaitAll faktycznie blokuje i czeka na zakończenie wszystkich innych zadań.

Zasadniczo Task.WhenAll da ci zadanie, które nie zostało ukończone, ale możesz użyć ContinueWith, gdy tylko określone zadania zakończą swoje wykonanie. Zauważ, że ani Task.WhenAll, ani Task.WaitAll faktycznie nie uruchomią zadań; tj. żadne zadania nie są uruchamiane tymi metodami. Oto sposób użycia ContinueWith z Task.WhenAll: 

Task.WhenAll (taskList) .ContinueWith (t => {

  // napisz tutaj swój kod

});

Zgodnie z dokumentacją Microsoftu, Task.WhenAll „tworzy zadanie, które zostanie zakończone po zakończeniu wszystkich obiektów Task w wyliczalnej kolekcji”.

Task.WhenAll kontra Task.WaitAll

Pozwólcie, że wyjaśnię różnicę między tymi dwiema metodami na prostym przykładzie. Załóżmy, że masz zadanie, które wykonuje jakąś czynność z wątkiem interfejsu użytkownika - powiedzmy, że w interfejsie użytkownika musi zostać wyświetlona animacja. Teraz, jeśli używasz Task.WaitAll, interfejs użytkownika zostanie zablokowany i nie będzie aktualizowany, dopóki wszystkie powiązane zadania nie zostaną ukończone, a blokada zwolniona. Jeśli jednak używasz Task.WhenAll w tej samej aplikacji, wątek interfejsu użytkownika nie zostanie zablokowany i zostanie zaktualizowany jak zwykle.

Więc kiedy z tych metod należy skorzystać? Cóż, możesz użyć WaitAll, gdy intencja jest synchronicznie blokowana, aby uzyskać wyniki. Ale jeśli chcesz skorzystać z asynchronii, powinieneś użyć wariantu WhenAll. Możesz czekać na Task.WhenAll bez konieczności blokowania bieżącego wątku. Dlatego możesz chcieć użyć await z Task.WhenAll wewnątrz metody async.

Podczas gdy Task.WaitAll blokuje bieżący wątek do momentu zakończenia wszystkich oczekujących zadań, Task.WhenAll zwraca obiekt zadania. Task.WaitAll zgłasza AggregateException, gdy co najmniej jedno zadanie zgłasza wyjątek. Gdy jedno lub więcej zadań zgłosi wyjątek i zaczekasz na metodę Task.WhenAll, rozpakowuje AggregateException i zwraca tylko pierwszy.

Unikaj używania Task.Run w pętlach

Możesz używać zadań, gdy chcesz wykonywać działania współbieżne. Jeśli potrzebujesz wysokiego stopnia równoległości, zadania nigdy nie są dobrym wyborem. Zawsze zaleca się unikanie używania wątków puli wątków w ASP.Net. Dlatego należy powstrzymać się od używania Task.Run lub Task.factory.StartNew w ASP.Net.

Task.Run powinno być zawsze używane w przypadku kodu związanego z procesorem. Task.Run nie jest dobrym wyborem w aplikacjach ASP.Net lub aplikacjach, które wykorzystują środowisko wykonawcze ASP.Net, ponieważ po prostu odciążają pracę wątku puli wątków. Jeśli korzystasz z interfejsu API sieci Web ASP.Net, żądanie korzystałoby już z wątku ThreadPool. Dlatego też, jeśli używasz Task.Run w swojej aplikacji ASP.Net Web API, po prostu ograniczasz skalowalność, przenosząc pracę do innego wątku roboczego bez żadnego powodu.

Zauważ, że używanie Task.Run w pętli ma wadę. Jeśli użyjesz metody Task.Run wewnątrz pętli, zostanie utworzonych wiele zadań - po jednym dla każdej jednostki pracy lub iteracji. Jeśli jednak używasz Parallel.ForEach zamiast używać Task.Run wewnątrz pętli, zostanie utworzony Partitioner, aby uniknąć tworzenia większej liczby zadań do wykonania czynności, niż jest to potrzebne. Może to znacznie poprawić wydajność, ponieważ można uniknąć zbyt wielu przełączników kontekstowych i nadal korzystać z wielu rdzeni w systemie.

Należy zauważyć, że Parallel.ForEach wewnętrznie używa Partitionera, aby dystrybuować kolekcję do elementów pracy. Nawiasem mówiąc, ta dystrybucja nie odbywa się dla każdego zadania na liście elementów, a raczej odbywa się jako partia. Zmniejsza to koszty ogólne, a tym samym poprawia wydajność. Innymi słowy, jeśli użyjesz Task.Run lub Task.Factory.StartNew wewnątrz pętli, utworzą nowe zadania jawnie dla każdej iteracji w pętli. Parallel.ForEach jest znacznie wydajniejszy, ponieważ zoptymalizuje wykonanie, rozkładając obciążenie pracą na wiele rdzeni w systemie.