Banyak jawaban bagus di sini, tetapi saya masih ingin memposting kata-kata kasar saya karena saya baru saja menemukan masalah yang sama dan melakukan penelitian. Atau lompat ke versi TLDR di bawah.
Masalah
Menunggu task
dikembalikan oleh Task.WhenAll
hanya melempar pengecualian pertama dari yang AggregateException
disimpan di task.Exception
, bahkan ketika beberapa tugas telah gagal.
Dokumen saat ini untukTask.WhenAll
mengatakan:
Jika salah satu tugas yang disediakan selesai dalam keadaan rusak, tugas yang dikembalikan juga akan selesai dalam keadaan rusak, di mana pengecualiannya akan berisi kumpulan dari kumpulan pengecualian yang tidak terbungkus dari setiap tugas yang disediakan.
Yang benar, tetapi tidak mengatakan apa-apa tentang perilaku "membuka" yang disebutkan di atas saat tugas yang dikembalikan menunggu.
Saya kira, dokumen tidak menyebutkannya karena perilaku itu tidak spesifikTask.WhenAll
.
Ini hanyalah Task.Exception
tipe AggregateException
dan untuk await
kelanjutan itu selalu dibuka sebagai pengecualian batin pertama, dengan desain. Ini bagus untuk kebanyakan kasus, karena biasanya Task.Exception
hanya terdiri dari satu pengecualian internal. Tetapi pertimbangkan kode ini:
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]);
}
Di sini, sebuah instance dari AggregateException
akan dibuka ke pengecualian dalam pertamanya InvalidOperationException
dengan cara yang persis sama seperti yang mungkin kita lakukan dengannya Task.WhenAll
. Kami bisa saja gagal mengamatiDivideByZeroException
jika kami tidak melalui task.Exception.InnerExceptions
langsung.
Stephen Toub dari Microsoft menjelaskan alasan di balik perilaku ini dalam masalah GitHub terkait :
Hal yang ingin saya sampaikan adalah bahwa hal itu dibahas secara mendalam, bertahun-tahun yang lalu, ketika ini pertama kali ditambahkan. Kami awalnya melakukan apa yang Anda sarankan, dengan Tugas yang dikembalikan dari WhenAll berisi satu AggregateException yang berisi semua pengecualian, yaitu tugas.Exception akan mengembalikan pembungkus AggregateException yang berisi AggregateException lain yang kemudian berisi pengecualian sebenarnya; lalu jika ditunggu, AggregateException dalam akan disebarkan. Umpan balik kuat yang kami terima yang menyebabkan kami mengubah desain adalah bahwa a) sebagian besar kasus semacam itu memiliki pengecualian yang cukup homogen, sehingga menyebarkan semua secara agregat tidaklah penting, b) menyebarkan agregat kemudian mematahkan ekspektasi seputar tangkapan untuk jenis pengecualian tertentu, dan c) untuk kasus di mana seseorang memang menginginkan kumpulan, mereka dapat melakukannya secara eksplisit dengan dua baris seperti yang saya tulis. Kami juga berdiskusi ekstensif tentang perilaku menunggu yang seharusnya terkait dengan tugas yang berisi banyak pengecualian, dan di sinilah kami mendarat.
Satu hal penting lainnya yang perlu diperhatikan, perilaku membuka bungkus ini dangkal. Yaitu, itu hanya akan membuka pengecualian pertama dari AggregateException.InnerExceptions
dan membiarkannya di sana, bahkan jika itu kebetulan merupakan contoh dari yang lain AggregateException
. Ini mungkin menambah lapisan kebingungan lainnya. Misalnya, mari kita ubah WhenAllWrong
seperti ini:
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));
}
Solusi (TLDR)
Jadi, kembali ke await Task.WhenAll(...)
, yang saya pribadi inginkan adalah bisa:
- Dapatkan satu pengecualian jika hanya satu yang terlempar;
- Dapatkan
AggregateException
jika lebih dari satu pengecualian telah dilemparkan secara kolektif oleh satu atau lebih tugas;
- Hindari menyimpan
Task
satu - satunya untuk memeriksa nya Task.Exception
;
- Menyebarkan status pembatalan benar (
Task.IsCanceled
), sebagai sesuatu seperti ini tidak akan melakukannya: Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }
.
Saya telah mengumpulkan ekstensi berikut untuk itu:
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();
}
}
Sekarang, berikut ini bekerja seperti yang saya inginkan:
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}");
}
AggregateException
. Jika Anda menggunakanTask.Wait
alih-alihawait
dalam contoh Anda, Anda akan menangkapAggregateException