W przypadku, gdy nie zależy mi na kolejności wykonywania zadań i po prostu potrzebuję ich wszystkich, czy nadal powinienem używać await Task.WhenAll
zamiast wielu await
? np. jest DoWork2
poniżej preferowanej metody DoWork1
(i dlaczego?):
using System;
using System.Threading.Tasks;
namespace ConsoleApp
{
class Program
{
static async Task<string> DoTaskAsync(string name, int timeout)
{
var start = DateTime.Now;
Console.WriteLine("Enter {0}, {1}", name, timeout);
await Task.Delay(timeout);
Console.WriteLine("Exit {0}, {1}", name, (DateTime.Now - start).TotalMilliseconds);
return name;
}
static async Task DoWork1()
{
var t1 = DoTaskAsync("t1.1", 3000);
var t2 = DoTaskAsync("t1.2", 2000);
var t3 = DoTaskAsync("t1.3", 1000);
await t1; await t2; await t3;
Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
}
static async Task DoWork2()
{
var t1 = DoTaskAsync("t2.1", 3000);
var t2 = DoTaskAsync("t2.2", 2000);
var t3 = DoTaskAsync("t2.3", 1000);
await Task.WhenAll(t1, t2, t3);
Console.WriteLine("DoWork2 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
}
static void Main(string[] args)
{
Task.WhenAll(DoWork1(), DoWork2()).Wait();
}
}
}
await t1; await t2; ....; await tn
=> druga jest zawsze najlepszym wyborem w obu przypadkachOdpowiedzi:
Tak, używaj,
WhenAll
ponieważ propaguje wszystkie błędy naraz. Dzięki wielokrotnym oczekiwaniom tracisz błędy, jeśli jeden z wcześniejszych rzutów oczekuje.Inną ważną różnicą jest to, że
WhenAll
będzie czekać na zakończenie wszystkich zadań, nawet w przypadku wystąpienia błędów (zadań z błędami lub anulowanych). Oczekiwanie ręcznie w sekwencji spowodowałoby nieoczekiwaną współbieżność, ponieważ część programu, która chce czekać, w rzeczywistości będzie działać wcześnie.Myślę, że ułatwia to również czytanie kodu, ponieważ wymagana semantyka jest bezpośrednio udokumentowana w kodzie.
źródło
await
jego wynikiem.Rozumiem, że głównym powodem preferowania
Task.WhenAll
wieluawait
s jest „ubijanie” wydajności / zadań:DoWork1
metoda działa mniej więcej tak:Z kolei
DoWork2
robi to:To, czy jest to wystarczająco ważna sprawa w twoim konkretnym przypadku, jest oczywiście „zależne od kontekstu” (przepraszam za kalambur).
źródło
Metoda asynchroniczna jest implementowana jako maszyna stanu. Możliwe jest pisanie metod tak, aby nie były kompilowane do maszyn stanu, jest to często nazywane szybką metodą asynchroniczną. Można je zaimplementować w następujący sposób:
Podczas korzystania
Task.WhenAll
można zachować ten kod przyspieszony, jednocześnie zapewniając, że dzwoniący jest w stanie czekać na zakończenie wszystkich zadań, np .:źródło
(Zastrzeżenie: ta odpowiedź jest zaczerpnięta / zainspirowana kursem TPL Async Iana Griffithsa w Pluralsight )
Innym powodem preferowania WhenAll jest obsługa wyjątków.
Załóżmy, że masz blok try-catch w metodach DoWork i przypuśćmy, że wywołują one różne metody DoTask:
W takim przypadku, jeśli wszystkie 3 zadania rzucają wyjątki, tylko pierwsze zostanie przechwycone. Każdy późniejszy wyjątek zostanie utracony. To znaczy, jeśli t2 i t3 rzucą wyjątek, tylko t2 zostanie przechwycony; itd. Kolejne wyjątki zadań pozostaną niezauważone.
Gdzie, tak jak w przypadku WhenAll - jeśli którekolwiek lub wszystkie zadania powodują błąd, wynikowe zadanie będzie zawierać wszystkie wyjątki. Słowo kluczowe await nadal zawsze zgłasza ponownie pierwszy wyjątek. Tak więc inne wyjątki są nadal skutecznie niezauważane. Jednym ze sposobów rozwiązania tego problemu jest dodanie pustej kontynuacji po zadaniu WhenAll i umieszczenie tam oczekiwania. W ten sposób, jeśli zadanie się nie powiedzie, właściwość result spowoduje zgłoszenie pełnego wyjątku agregacji:
źródło
Inne odpowiedzi na to pytanie podają przyczyny techniczne
await Task.WhenAll(t1, t2, t3);
jest to preferowane. Ta odpowiedź będzie miała na celu spojrzenie na to z łagodniejszej strony (do której nawiązuje @usr), jednocześnie dochodząc do tego samego wniosku.await Task.WhenAll(t1, t2, t3);
jest podejściem bardziej funkcjonalnym, ponieważ deklaruje zamiar i jest atomowe.W
await t1; await t2; await t3;
przypadku nic nie stoi na przeszkodzie, aby członek zespołu (a może nawet twoje przyszłe ja!) Mógł dodać kod między poszczególnymiawait
instrukcjami. Jasne, skompresowałeś go do jednej linii, aby zasadniczo to osiągnąć, ale to nie rozwiązuje problemu. Poza tym generalnie jest to zła forma w ustawieniach zespołowych, aby zawierać wiele instrukcji w danym wierszu kodu, ponieważ może to utrudniać skanowanie pliku źródłowego dla ludzkich oczu.Mówiąc najprościej,
await Task.WhenAll(t1, t2, t3);
jest łatwiejszy w utrzymaniu, ponieważ jaśniej przekazuje twoje zamiary i jest mniej podatny na osobliwe błędy, które mogą wynikać z dobrych intencji aktualizacji kodu, a nawet po prostu nieudane scalanie.źródło