W tym kodzie:
private async void button1_Click(object sender, EventArgs e) {
try {
await Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2());
}
catch (Exception ex) {
// Expect AggregateException, but got InvalidTimeZoneException
}
}
Task DoLongThingAsyncEx1() {
return Task.Run(() => { throw new InvalidTimeZoneException(); });
}
Task DoLongThingAsyncEx2() {
return Task.Run(() => { throw new InvalidOperation();});
}
Spodziewałem WhenAll
się, że utworzę i wyrzucę AggregateException
, ponieważ co najmniej jedno z zadań, na które oczekiwał, rzuciło wyjątek. Zamiast tego otrzymuję pojedynczy wyjątek zgłoszony przez jedno z zadań.
Nie WhenAll
zawsze tworzy AggregateException
?
.net
exception
asynchronous
tap
Michael Ray Lovett
źródło
źródło
AggregateException
. Gdybyś użyłTask.Wait
zamiastawait
w swoim przykładzie, złapałbyśAggregateException
Task.WhenAll
i wpadłem w tę samą pułapkę. Więc spróbowałem zagłębić się w szczegóły dotyczące tego zachowania.Odpowiedzi:
Nie pamiętam dokładnie gdzie, ale gdzieś przeczytałem, że z nowymi słowami kluczowymi async / await rozpakowują one
AggregateException
w rzeczywisty wyjątek.Tak więc w bloku catch otrzymujesz rzeczywisty wyjątek, a nie zagregowany. Pomaga nam to pisać bardziej naturalny i intuicyjny kod.
Było to również potrzebne do łatwiejszej konwersji istniejącego kodu do korzystania z async / await, gdzie duża część kodu oczekuje określonych wyjątków, a nie zagregowanych wyjątków.
-- Edytować --
Rozumiem:
An Async Primer autorstwa Billa Wagnera
źródło
Wiem, że jest to pytanie, na które już udzielono odpowiedzi, ale wybrana odpowiedź tak naprawdę nie rozwiązuje problemu OP, więc pomyślałem, że opublikuję to.
To rozwiązanie zapewnia zagregowany wyjątek (tj. Wszystkie wyjątki, które zostały zgłoszone przez różne zadania) i nie blokuje (przepływ pracy jest nadal asynchroniczny).
async Task Main() { var task = Task.WhenAll(A(), B()); try { var results = await task; Console.WriteLine(results); } catch (Exception) { if (task.Exception != null) { throw task.Exception; } } } public async Task<int> A() { await Task.Delay(100); throw new Exception("A"); } public async Task<int> B() { await Task.Delay(100); throw new Exception("B"); }
Kluczem jest zapisanie odwołania do zadania zagregowanego, zanim zaczekasz, a następnie możesz uzyskać dostęp do jego właściwości Exception, która przechowuje Twój AggregateException (nawet jeśli tylko jedno zadanie zgłosiło wyjątek).
Mam nadzieję, że to jest nadal przydatne. Wiem, że miałem dzisiaj ten problem.
źródło
throw task.Exception;
wewnątrzcatch
bloku? (Wprawia mnie w zakłopotanie, gdy widzę pusty haczyk, gdy wyjątki są faktycznie obsługiwane.)Task.IsCanceled
) nie jest poprawnie propagowany. To może być rozwiązuje przy użyciu pomocnika wewnętrzny jak ten .Możesz przejść przez wszystkie zadania, aby sprawdzić, czy więcej niż jedno zgłosiło wyjątek:
private async Task Example() { var tasks = new [] { DoLongThingAsyncEx1(), DoLongThingAsyncEx2() }; try { await Task.WhenAll(tasks); } catch (Exception ex) { var exceptions = tasks.Where(t => t.Exception != null) .Select(t => t.Exception); } } private Task DoLongThingAsyncEx1() { return Task.Run(() => { throw new InvalidTimeZoneException(); }); } private Task DoLongThingAsyncEx2() { return Task.Run(() => { throw new InvalidOperationException(); }); }
źródło
WhenAll
kończy działanie po pierwszym wyjątku i zwraca go. patrz: stackoverflow.com/questions/6123406/waitall-vs-whenallexceptions
zawiera oba zgłoszone wyjątki.await
powoduje rozpakowanie pierwszego wyjątku, ale wszystkie wyjątki są rzeczywiście nadal dostępne za pośrednictwem tablicy Tasks.Pomyślałem, że rozszerzę odpowiedź @ Richiban, aby powiedzieć, że możesz również obsłużyć AggregateException w bloku catch, odwołując się do niego z zadania. Na przykład:
async Task Main() { var task = Task.WhenAll(A(), B()); try { var results = await task; Console.WriteLine(results); } catch (Exception ex) { // This doesn't fire until both tasks // are complete. I.e. so after 10 seconds // as per the second delay // The ex in this instance is the first // exception thrown, i.e. "A". var firstExceptionThrown = ex; // This aggregate contains both "A" and "B". var aggregateException = task.Exception; } } public async Task<int> A() { await Task.Delay(100); throw new Exception("A"); } public async Task<int> B() { // Extra delay to make it clear that the await // waits for all tasks to complete, including // waiting for this exception. await Task.Delay(10000); throw new Exception("B"); }
źródło
Myślisz o
Task.WaitAll
- rzucaAggregateException
.WhenAll po prostu zgłasza pierwszy wyjątek z listy wyjątków, które napotka.
źródło
WhenAll
metody maException
właściwośćAggregateException
zawierającą wszystkie wyjątki zgłoszone w plikuInnerExceptions
. To, co się tutaj dzieje, toawait
rzucanie pierwszego wewnętrznego wyjątku zamiastAggregateException
samego siebie (jak powiedział decyklon). Wywołanie metody zadaniaWait
zamiast czekania powoduje zgłoszenie oryginalnego wyjątku.Wiele dobrych odpowiedzi tutaj, ale nadal chciałbym opublikować mój rant, ponieważ właśnie natknąłem się na ten sam problem i przeprowadziłem badania. Lub przejdź do wersji TLDR poniżej.
Problem
Oczekiwanie na
task
zwrócone przezTask.WhenAll
tylko zgłasza pierwszy wyjątekAggregateException
przechowywanego wtask.Exception
, nawet jeśli wystąpił błąd wielu zadań.Te obecne docs dla
Task.WhenAll
słownie:Co jest poprawne, ale nie mówi nic o wspomnianym wcześniej zachowaniu „rozpakowywania”, kiedy oczekuje się zwróconego zadania.
Przypuszczam, że doktorzy nie wspominają o tym, ponieważ to zachowanie nie jest specyficzne dla
Task.WhenAll
.Jest to po prostu
Task.Exception
typAggregateException
i dlaawait
kontynuacji zawsze zostaje rozpakowane jako pierwszy wewnętrzny wyjątek, zgodnie z projektem. Jest to świetne w większości przypadków, ponieważ zwykleTask.Exception
składa się tylko z jednego wewnętrznego wyjątku. Ale rozważ ten kod:Task WhenAllWrong() { var tcs = new TaskCompletionSource<DBNull>(); tcs.TrySetException(new Exception[] { new InvalidOperationException(), new DivideByZeroException() }); return tcs.Task; } var task = WhenAllWrong(); try { await task; } catch (Exception exception) { // task.Exception is an AggregateException with 2 inner exception Assert.IsTrue(task.Exception.InnerExceptions.Count == 2); Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(InvalidOperationException)); Assert.IsInstanceOfType(task.Exception.InnerExceptions[1], typeof(DivideByZeroException)); // However, the exception that we caught here is // the first exception from the above InnerExceptions list: Assert.IsInstanceOfType(exception, typeof(InvalidOperationException)); Assert.AreSame(exception, task.Exception.InnerExceptions[0]); }
W tym przypadku wystąpienie polecenia
AggregateException
zostaje rozpakowane do pierwszego wewnętrznego wyjątkuInvalidOperationException
w dokładnie taki sam sposób, w jaki moglibyśmy go miećTask.WhenAll
. Moglibyśmy nie obserwowaćDivideByZeroException
, gdybyśmy nie przeszlitask.Exception.InnerExceptions
bezpośrednio.Stephen Toub z Microsoftu wyjaśnia przyczynę tego zachowania w powiązanym problemie z GitHub :
Jeszcze jedna ważna rzecz, na którą należy zwrócić uwagę, to zachowanie podczas rozpakowywania jest płytkie. Oznacza to, że tylko rozpakuje pierwszy wyjątek
AggregateException.InnerExceptions
i pozostawi go tam, nawet jeśli jest to wystąpienie innegoAggregateException
. Może to dodać kolejną warstwę zamieszania. Na przykład zmieńmy wWhenAllWrong
ten sposób:async Task WhenAllWrong() { await Task.FromException(new AggregateException( new InvalidOperationException(), new DivideByZeroException())); } var task = WhenAllWrong(); try { await task; } catch (Exception exception) { // now, task.Exception is an AggregateException with 1 inner exception, // which is itself an instance of AggregateException Assert.IsTrue(task.Exception.InnerExceptions.Count == 1); Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(AggregateException)); // And now the exception that we caught here is that inner AggregateException, // which is also the same object we have thrown from WhenAllWrong: var aggregate = exception as AggregateException; Assert.IsNotNull(aggregate); Assert.AreSame(exception, task.Exception.InnerExceptions[0]); Assert.IsInstanceOfType(aggregate.InnerExceptions[0], typeof(InvalidOperationException)); Assert.IsInstanceOfType(aggregate.InnerExceptions[1], typeof(DivideByZeroException)); }
Rozwiązanie (TLDR)
Wracając do
await Task.WhenAll(...)
tego, czego osobiście chciałem, to móc:AggregateException
jeśli więcej niż jeden wyjątek został zgłoszony łącznie przez jedno lub więcej zadań;Task
jedynego w celu sprawdzenia jegoTask.Exception
;Task.IsCanceled
), a coś takiego nie zrobi, że:Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }
.Przygotowałem do tego następujące rozszerzenie:
public static class TaskExt { /// <summary> /// A workaround for getting all of AggregateException.InnerExceptions with try/await/catch /// </summary> public static Task WithAggregatedExceptions(this Task @this) { // using AggregateException.Flatten as a bonus return @this.ContinueWith( continuationFunction: anteTask => anteTask.IsFaulted && anteTask.Exception is AggregateException ex && (ex.InnerExceptions.Count > 1 || ex.InnerException is AggregateException) ? Task.FromException(ex.Flatten()) : anteTask, cancellationToken: CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, scheduler: TaskScheduler.Default).Unwrap(); } }
Teraz działa tak, jak chcę:
try { await Task.WhenAll( Task.FromException(new InvalidOperationException()), Task.FromException(new DivideByZeroException())) .WithAggregatedExceptions(); } catch (OperationCanceledException) { Trace.WriteLine("Canceled"); } catch (AggregateException exception) { Trace.WriteLine("2 or more exceptions"); // Now the exception that we caught here is an AggregateException, // with two inner exceptions: var aggregate = exception as AggregateException; Assert.IsNotNull(aggregate); Assert.IsInstanceOfType(aggregate.InnerExceptions[0], typeof(InvalidOperationException)); Assert.IsInstanceOfType(aggregate.InnerExceptions[1], typeof(DivideByZeroException)); } catch (Exception exception) { Trace.WriteLine($"Just a single exception: ${exception.Message}"); }
źródło
To działa dla mnie
private async Task WhenAllWithExceptions(params Task[] tasks) { var result = await Task.WhenAll(tasks); if (result.IsFaulted) { throw result.Exception; } }
źródło
WhenAll
to nie to samo coWhenAny
.await Task.WhenAny(tasks)
zakończy się po zakończeniu dowolnego zadania. Jeśli więc masz jedno zadanie, które kończy się natychmiast i kończy się powodzeniem, a inne zajmuje kilka sekund, zanim zgłosi wyjątek, zostanie ono zwrócone natychmiast bez żadnego błędu.W Twoim kodzie pierwszy wyjątek jest zwracany zgodnie z projektem, jak wyjaśniono na stronie http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/task-exception-handling-in-net-4-5. aspx
Jeśli chodzi o twoje pytanie, otrzymasz AggreateException, jeśli napiszesz taki kod:
try { var result = Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2()).Result; } catch (Exception ex) { // Expect AggregateException here }
źródło