Mengapa tidak menunggu di Task.WhenAll melempar AggregateException?

103

Dalam kode ini:

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();});
}

Saya berharap WhenAlluntuk membuat dan melempar AggregateException, karena setidaknya salah satu tugas yang ditunggu adalah pengecualian. Sebaliknya, saya mendapatkan kembali satu pengecualian yang dilemparkan oleh salah satu tugas.

Tidak WhenAllselalu membuat AggregateException?

Michael Ray Lovett
sumber
7
WhenAll memang membuat file AggregateException. Jika Anda menggunakan Task.Waitalih-alih awaitdalam contoh Anda, Anda akan menangkapAggregateException
Peter Ritchie
2
+1, inilah yang saya coba cari tahu, hemat saya berjam-jam debugging dan google-ing.
kennyzx
Untuk pertama kalinya dalam beberapa tahun saya membutuhkan semua pengecualian Task.WhenAll, dan saya jatuh ke dalam perangkap yang sama. Jadi saya sudah mencoba menjelaskan secara detail tentang perilaku ini.
noseratio

Jawaban:

76

Saya tidak begitu ingat di mana, tetapi saya membaca di suatu tempat bahwa dengan kata kunci async / await baru , kata kunci tersebut membuka AggregateExceptionke dalam pengecualian yang sebenarnya.

Jadi, dalam blok tangkap, Anda mendapatkan pengecualian sebenarnya dan bukan gabungan. Ini membantu kami menulis kode yang lebih alami dan intuitif.

Ini juga diperlukan untuk memudahkan konversi kode yang ada menjadi menggunakan async / await di mana banyak kode mengharapkan pengecualian tertentu dan bukan pengecualian gabungan.

- Edit -

Mengerti:

An Async Primer oleh Bill Wagner

Bill Wagner berkata: (dalam When Exception Happen )

... Saat Anda menggunakan await, kode yang dihasilkan oleh compiler akan membuka AggregateException dan menampilkan pengecualian yang mendasarinya. Dengan memanfaatkan await, Anda menghindari pekerjaan tambahan untuk menangani jenis AggregateException yang digunakan oleh Task.Result, Task.Wait, dan metode Tunggu lainnya yang ditentukan di kelas Task. Itu alasan lain untuk menggunakan await daripada metode Tugas yang mendasarinya ....

deklon
sumber
3
Ya, saya tahu ada beberapa perubahan pada penanganan pengecualian, tetapi dokumen terbaru untuk Task.WhenAll menyatakan "Jika salah satu tugas yang diberikan selesai dalam keadaan salah, tugas yang dikembalikan juga akan selesai dalam status Salah, di mana pengecualiannya akan berisi agregasi kumpulan pengecualian yang tidak terbungkus dari masing-masing tugas yang diberikan ".... Dalam kasus saya, kedua tugas saya selesai dalam keadaan salah ...
Michael Ray Lovett
4
@MichaelRayLovett: Anda tidak menyimpan Tugas yang dikembalikan di mana pun. Saya yakin ketika Anda melihat properti Exception dari tugas itu, Anda akan mendapatkan AggregateException. Namun, dalam kode Anda, Anda menggunakan await. Itu membuat AggregateException dibuka ke dalam pengecualian sebenarnya.
deklon
3
Saya memikirkannya juga, tetapi dua masalah muncul: 1) Sepertinya saya tidak dapat menemukan cara menyimpan tugas sehingga saya dapat memeriksanya (yaitu "Task myTask = await Task.WhenAll (...)" tidak Sepertinya tidak berhasil. dan 2) Saya rasa saya tidak melihat bagaimana menunggu bisa mewakili beberapa pengecualian hanya sebagai satu pengecualian .. pengecualian mana yang harus dilaporkan? Pilih satu secara acak?
Michael Ray Lovett
2
Ya, ketika saya menyimpan tugas dan memeriksanya di try / catch dari menunggu, saya melihat pengecualiannya adalah AggregatedException. Jadi dokumen yang saya baca benar; Task.WhenAll membungkus pengecualian dalam AggregateException. Tapi kemudian menunggu adalah membuka bungkusnya. Saya membaca artikel Anda sekarang, tapi saya belum melihat bagaimana menunggu dapat memilih satu pengecualian dari AggregateExceptions dan melemparkan yang satu itu ke yang lain ..
Michael Ray Lovett
3
Baca artikelnya, terima kasih. Tapi saya masih tidak mengerti mengapa await mewakili AggregateException (mewakili beberapa pengecualian) hanya sebagai satu pengecualian. Bagaimana penanganan pengecualian yang komprehensif? .. Saya kira jika saya ingin tahu persis tugas mana yang memberikan pengecualian dan mana yang mereka lemparkan, saya harus memeriksa objek Tugas yang dibuat oleh Task.WhenAll ??
Michael Ray Lovett
56

Saya tahu ini adalah pertanyaan yang sudah dijawab tetapi jawaban yang dipilih tidak benar - benar menyelesaikan masalah OP, jadi saya pikir saya akan memposting ini.

Solusi ini memberi Anda pengecualian agregat (yaitu semua pengecualian yang dilemparkan oleh berbagai tugas) dan tidak memblokir (alur kerja masih asinkron).

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");
}

Kuncinya adalah menyimpan referensi ke tugas agregat sebelum Anda menunggunya, lalu Anda dapat mengakses properti Exception yang menyimpan AggregateException Anda (meskipun hanya satu tugas yang memberikan pengecualian).

Semoga masih bermanfaat. Saya tahu saya mengalami masalah ini hari ini.

Richiban
sumber
Jawaban jelas yang sangat bagus, ini harus IMO yang dipilih.
bytedev
3
+1, tetapi tidak bisakah Anda memasukkan begitu saja ke throw task.Exception;dalam catchblok? (Saya bingung melihat tangkapan kosong saat pengecualian benar-benar ditangani.)
AnorZaken
@AnorDibuat sepenuhnya; Saya tidak ingat mengapa saya menulisnya seperti itu pada awalnya, tetapi saya tidak dapat melihat sisi negatifnya jadi saya telah memindahkannya ke blok tangkapan. Terima kasih
Richiban
Satu kelemahan kecil dari pendekatan ini adalah status pembatalan ( Task.IsCanceled) tidak disebarkan dengan benar. Ini bisa diselesaikan dengan bantuan ekstensi seperti ini .
noseratio
34

Anda dapat melintasi semua tugas untuk melihat apakah ada lebih dari satu yang membuat pengecualian:

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(); });
}
jgauffin.dll
sumber
2
ini tidak bekerja. WhenAllkeluar setelah pengecualian pertama dan mengembalikannya. lihat: stackoverflow.com/questions/6123406/waitall-vs-whenall
jenson-button-event
14
Dua komentar sebelumnya salah. Kode sebenarnya berfungsi dan exceptionsberisi kedua pengecualian yang dilempar.
Tobias
DoLongThingAsyncEx2 () harus menampilkan InvalidOperationException () baru, bukan InvalidOperation () baru
Artemious
9
Untuk mengurangi keraguan di sini, saya mengumpulkan biola panjang yang diharapkan menunjukkan dengan tepat bagaimana penanganan ini terjadi: dotnetfiddle.net/X2AOvM . Anda dapat melihat bahwa awaitpenyebab pengecualian pertama dibuka, tetapi semua pengecualian memang masih tersedia melalui larik Tugas.
nuclearpidgeon
13

Hanya berpikir saya akan memperluas jawaban @ Richiban untuk mengatakan bahwa Anda juga dapat menangani AggregateException di blok tangkap dengan mereferensikannya dari tugas. Misalnya:

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");
}
Daniel Šmon
sumber
11

Anda sedang memikirkan Task.WaitAll- itu melempar AggregateException.

WhenAll hanya menampilkan pengecualian pertama dari daftar pengecualian yang ditemuinya.

Mohit Datta
sumber
3
Ini salah, tugas yang dikembalikan dari WhenAllmetode memiliki Exceptionproperti yang AggregateExceptionberisi semua pengecualian yang dilemparkan ke dalamnya InnerExceptions. Apa yang terjadi di sini adalah awaitmembuang pengecualian dalam pertama, bukan pengecualian AggregateExceptionitu sendiri (seperti kata decyclone). Memanggil metode tugas Waitalih-alih menunggunya menyebabkan pengecualian asli dilempar.
Şafak Gür
4

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 taskdikembalikan oleh Task.WhenAllhanya melempar pengecualian pertama dari yang AggregateExceptiondisimpan 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.Exceptiontipe AggregateExceptiondan untuk awaitkelanjutan itu selalu dibuka sebagai pengecualian batin pertama, dengan desain. Ini bagus untuk kebanyakan kasus, karena biasanya Task.Exceptionhanya 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 AggregateExceptionakan dibuka ke pengecualian dalam pertamanya InvalidOperationExceptiondengan cara yang persis sama seperti yang mungkin kita lakukan dengannya Task.WhenAll. Kami bisa saja gagal mengamatiDivideByZeroException jika kami tidak melalui task.Exception.InnerExceptionslangsung.

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.InnerExceptionsdan membiarkannya di sana, bahkan jika itu kebetulan merupakan contoh dari yang lain AggregateException. Ini mungkin menambah lapisan kebingungan lainnya. Misalnya, mari kita ubah WhenAllWrongseperti 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 AggregateExceptionjika lebih dari satu pengecualian telah dilemparkan secara kolektif oleh satu atau lebih tugas;
  • Hindari menyimpan Tasksatu - 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}");
}
noseratio
sumber
2
Jawaban yang fantastis
gulungan
-3

Ini berhasil untuk saya

private async Task WhenAllWithExceptions(params Task[] tasks)
{
    var result = await Task.WhenAll(tasks);
    if (result.IsFaulted)
    {
                throw result.Exception;
    }
}
Alexey Kulikov
sumber
1
WhenAlltidak sama dengan WhenAny. await Task.WhenAny(tasks)akan selesai segera setelah tugas selesai. Jadi, jika Anda memiliki satu tugas yang segera selesai dan berhasil dan tugas lainnya membutuhkan waktu beberapa detik sebelum memberikan pengecualian, ini akan segera dikembalikan tanpa kesalahan apa pun.
StriplingWarrior
Maka garis lemparan tidak akan pernah mengenai di sini - WhenAll akan mengeluarkan pengecualian
thab
-5

Dalam kode Anda, pengecualian pertama dikembalikan menurut desain seperti yang dijelaskan di http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/task-exception-handling-in-net-4-5. aspx

Sedangkan untuk pertanyaan Anda, Anda akan mendapatkan AggreateException jika Anda menulis kode seperti ini:

try {
    var result = Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2()).Result; 
}
catch (Exception ex) {
    // Expect AggregateException here
} 
Nebula
sumber