Tangkap pengecualian yang dilemparkan dengan metode batal async

283

Menggunakan CTP async dari Microsoft untuk .NET, apakah mungkin untuk menangkap pengecualian yang dilemparkan oleh metode async dalam metode panggilan?

public async void Foo()
{
    var x = await DoSomethingAsync();

    /* Handle the result, but sometimes an exception might be thrown.
       For example, DoSomethingAsync gets data from the network
       and the data is invalid... a ProtocolException might be thrown. */
}

public void DoFoo()
{
    try
    {
        Foo();
    }
    catch (ProtocolException ex)
    {
          /* The exception will never be caught.
             Instead when in debug mode, VS2010 will warn and continue.
             The deployed the app will simply crash. */
    }
}

Jadi pada dasarnya saya ingin pengecualian dari kode async untuk muncul ke kode panggilan saya jika itu mungkin sama sekali.

TimothyP
sumber
1
Apakah ini memberi Anda bantuan? social.msdn.microsoft.com/Forums/en/async/thread/…
svrist
22
Jika ada yang tersandung pada hal ini di masa depan, artikel Async / Await Best Practices ... memiliki penjelasan yang baik dalam "Pengecualian Gambar 2 dari Metode Void Async yang Tidak Dapat Ditangkap dengan Catch". " Ketika pengecualian dilempar keluar dari tugas Async Task atau metode Tugas async <T>, pengecualian itu ditangkap dan ditempatkan pada objek Tugas. Dengan metode batal async, tidak ada objek Tugas, setiap pengecualian dilempar keluar dari metode batal async. akan dinaikkan langsung pada SynchronizationContext yang aktif ketika metode batal async dimulai. "
Mr Moose
Anda dapat menggunakan pendekatan ini atau ini
Tselofan

Jawaban:

263

Ini agak aneh untuk dibaca tetapi ya, pengecualian akan muncul hingga kode panggilan - tetapi hanya jika Anda awaitatau Wait()panggilan ituFoo .

public async Task Foo()
{
    var x = await DoSomethingAsync();
}

public async void DoFoo()
{
    try
    {
        await Foo();
    }
    catch (ProtocolException ex)
    {
          // The exception will be caught because you've awaited
          // the call in an async method.
    }
}

//or//

public void DoFoo()
{
    try
    {
        Foo().Wait();
    }
    catch (ProtocolException ex)
    {
          /* The exception will be caught because you've
             waited for the completion of the call. */
    }
} 

Metode async void memiliki semantik penanganan kesalahan yang berbeda. Ketika pengecualian dilempar keluar dari tugas async atau metode tugas async, pengecualian itu ditangkap dan ditempatkan pada objek tugas. Dengan metode batal async, tidak ada objek Tugas, jadi setiap pengecualian yang dibuang dari metode batal async akan dimunculkan langsung pada SynchronizationContext yang aktif ketika metode batal async dimulai. - https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Perhatikan bahwa menggunakan Tunggu () dapat menyebabkan aplikasi Anda diblokir, jika .Net memutuskan untuk menjalankan metode Anda secara sinkron.

Penjelasan ini http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions cukup bagus - ini membahas langkah-langkah yang dilakukan oleh kompiler untuk mencapai keajaiban ini.

Stuart
sumber
3
Maksud saya sebenarnya mudah untuk membaca - sedangkan saya tahu apa yang sebenarnya terjadi benar-benar rumit - jadi otak saya mengatakan kepada saya untuk tidak mempercayai mata saya ...
Stuart
8
Saya pikir metode Foo () harus ditandai sebagai Tugas bukan batal.
Sornii
4
Saya cukup yakin ini akan menghasilkan AggregateException. Dengan demikian, blok tangkap seperti yang muncul dalam jawaban ini tidak akan menangkap pengecualian.
xanadont
2
"tetapi hanya jika Anda menunggu atau Tunggu () panggilan ke Foo" Bagaimana Anda bisa awaitmenelepon ke Foo, ketika Foo kembali batal? async void Foo(). Type void is not awaitable?
rism
3
Tidak bisa menunggu metode batal, kan?
Hitesh P
74

Alasan pengecualian tidak ditangkap adalah karena metode Foo () memiliki tipe pengembalian kosong dan jadi ketika menunggu dipanggil, ia hanya kembali. Karena DoFoo () tidak menunggu penyelesaian Foo, pengendali pengecualian tidak dapat digunakan.

Ini membuka solusi yang lebih sederhana jika Anda dapat mengubah tanda tangan metode - ubah Foo()sehingga mengembalikan tipe Taskdan kemudian DoFoo()bisa await Foo(), seperti dalam kode ini:

public async Task Foo() {
    var x = await DoSomethingThatThrows();
}

public async void DoFoo() {
    try {
        await Foo();
    } catch (ProtocolException ex) {
        // This will catch exceptions from DoSomethingThatThrows
    }
}
Gereja Rob
sumber
19
Ini benar-benar dapat menyelinap pada Anda dan harus diperingatkan oleh kompiler.
GGleGrand
19

Kode Anda tidak melakukan apa yang Anda pikirkan. Metode Async kembali segera setelah metode mulai menunggu hasil async. Wawasan untuk menggunakan pelacakan untuk menyelidiki bagaimana kode sebenarnya berperilaku.

Kode di bawah ini melakukan yang berikut:

  • Buat 4 tugas
  • Setiap tugas secara asinkron akan menambah nomor dan mengembalikan nomor yang ditambahkan
  • Ketika hasil async telah tiba itu dilacak.

 

static TypeHashes _type = new TypeHashes(typeof(Program));        
private void Run()
{
    TracerConfig.Reset("debugoutput");

    using (Tracer t = new Tracer(_type, "Run"))
    {
        for (int i = 0; i < 4; i++)
        {
            DoSomeThingAsync(i);
        }
    }
    Application.Run();  // Start window message pump to prevent termination
}


private async void DoSomeThingAsync(int i)
{
    using (Tracer t = new Tracer(_type, "DoSomeThingAsync"))
    {
        t.Info("Hi in DoSomething {0}",i);
        try
        {
            int result = await Calculate(i);
            t.Info("Got async result: {0}", result);
        }
        catch (ArgumentException ex)
        {
            t.Error("Got argument exception: {0}", ex);
        }
    }
}

Task<int> Calculate(int i)
{
    var t = new Task<int>(() =>
    {
        using (Tracer t2 = new Tracer(_type, "Calculate"))
        {
            if( i % 2 == 0 )
                throw new ArgumentException(String.Format("Even argument {0}", i));
            return i++;
        }
    });
    t.Start();
    return t;
}

Saat Anda mengamati jejaknya

22:25:12.649  02172/02820 {          AsyncTest.Program.Run 
22:25:12.656  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.657  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 0    
22:25:12.658  02172/05220 {          AsyncTest.Program.Calculate    
22:25:12.659  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.659  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 1    
22:25:12.660  02172/02756 {          AsyncTest.Program.Calculate    
22:25:12.662  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.662  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 2    
22:25:12.662  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.662  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 3    
22:25:12.664  02172/02756          } AsyncTest.Program.Calculate Duration 4ms   
22:25:12.666  02172/02820          } AsyncTest.Program.Run Duration 17ms  ---- Run has completed. The async methods are now scheduled on different threads. 
22:25:12.667  02172/02756 Information AsyncTest.Program.DoSomeThingAsync Got async result: 1    
22:25:12.667  02172/02756          } AsyncTest.Program.DoSomeThingAsync Duration 8ms    
22:25:12.667  02172/02756 {          AsyncTest.Program.Calculate    
22:25:12.665  02172/05220 Exception   AsyncTest.Program.Calculate Exception thrown: System.ArgumentException: Even argument 0   
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     
22:25:12.668  02172/02756 Exception   AsyncTest.Program.Calculate Exception thrown: System.ArgumentException: Even argument 2   
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     
22:25:12.724  02172/05220          } AsyncTest.Program.Calculate Duration 66ms      
22:25:12.724  02172/02756          } AsyncTest.Program.Calculate Duration 57ms      
22:25:12.725  02172/05220 Error       AsyncTest.Program.DoSomeThingAsync Got argument exception: System.ArgumentException: Even argument 0  

Server stack trace:     
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     

Exception rethrown at [0]:      
   at System.Runtime.CompilerServices.TaskAwaiter.EndAwait()    
   at System.Runtime.CompilerServices.TaskAwaiter`1.EndAwait()  
   at AsyncTest.Program.DoSomeThingAsyncd__8.MoveNext() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 106    
22:25:12.725  02172/02756 Error       AsyncTest.Program.DoSomeThingAsync Got argument exception: System.ArgumentException: Even argument 2  

Server stack trace:     
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     

Exception rethrown at [0]:      
   at System.Runtime.CompilerServices.TaskAwaiter.EndAwait()    
   at System.Runtime.CompilerServices.TaskAwaiter`1.EndAwait()  
   at AsyncTest.Program.DoSomeThingAsyncd__8.MoveNext() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 0      
22:25:12.726  02172/05220          } AsyncTest.Program.DoSomeThingAsync Duration 70ms   
22:25:12.726  02172/02756          } AsyncTest.Program.DoSomeThingAsync Duration 64ms   
22:25:12.726  02172/05220 {          AsyncTest.Program.Calculate    
22:25:12.726  02172/05220          } AsyncTest.Program.Calculate Duration 0ms   
22:25:12.726  02172/05220 Information AsyncTest.Program.DoSomeThingAsync Got async result: 3    
22:25:12.726  02172/05220          } AsyncTest.Program.DoSomeThingAsync Duration 64ms   

Anda akan melihat bahwa metode Jalankan selesai pada utas 2820 sementara hanya satu utas anak telah selesai (2756). Jika Anda mencoba / menangkap metode menunggu Anda, Anda dapat "menangkap" pengecualian dengan cara biasa meskipun kode Anda dieksekusi di utas lain ketika tugas perhitungan telah selesai dan kontiuasi Anda dieksekusi.

Metode perhitungan melacak pengecualian yang dilemparkan secara otomatis karena saya memang menggunakan ApiChange.Api.dll dari alat ApiChange . Tracing dan Reflector sangat membantu untuk memahami apa yang sedang terjadi. Untuk menghilangkan threading, Anda dapat membuat GetAwaiter BeginAwait dan EndAwait versi Anda sendiri dan membungkus bukan tugas tetapi mis. Malas dan lacak di dalam metode ekstensi Anda sendiri. Maka Anda akan jauh lebih memahami apa yang dikompilasi dan apa yang dilakukan TPL.

Sekarang Anda melihat bahwa tidak ada cara untuk mencoba / menangkap pengecualian Anda kembali karena tidak ada bingkai tumpukan tersisa untuk pengecualian untuk merambat dari. Kode Anda mungkin melakukan sesuatu yang sama sekali berbeda setelah Anda melakukan operasi async. Mungkin memanggil Thread. Tidur atau bahkan mengakhiri. Selama masih ada satu foreground thread kiri aplikasi Anda akan dengan senang hati melanjutkan untuk melakukan tugas-tugas yang tidak sinkron.


Anda dapat menangani pengecualian di dalam metode async setelah operasi asinkron Anda selesai dan memanggil kembali ke utas UI. Cara yang disarankan untuk melakukan ini adalah dengan TaskScheduler.FromSynchronizationContext . Itu hanya berfungsi jika Anda memiliki utas UI dan tidak terlalu sibuk dengan hal-hal lain.

Alois Kraus
sumber
5

Pengecualian dapat ditangkap dalam fungsi async.

public async void Foo()
{
    try
    {
        var x = await DoSomethingAsync();
        /* Handle the result, but sometimes an exception might be thrown
           For example, DoSomethingAsync get's data from the network
           and the data is invalid... a ProtocolException might be thrown */
    }
    catch (ProtocolException ex)
    {
          /* The exception will be caught here */
    }
}

public void DoFoo()
{
    Foo();
}
Sanjeevakumar Hiremath
sumber
2
Hai, saya tahu tetapi saya sangat membutuhkan informasi itu di DoFoo sehingga saya dapat menampilkan informasi di UI. Dalam hal ini penting bagi UI untuk menampilkan pengecualian karena ini bukan alat pengguna akhir tetapi alat untuk men-debug protokol komunikasi
TimothyP
Dalam hal ini, panggilan balik masuk akal (delegasi async tua yang baik)
Sanjeevakumar Hiremath
@Tim: Sertakan informasi apa pun yang Anda butuhkan dalam pengecualian yang dilemparkan?
Eric J.
5

Penting juga untuk dicatat bahwa Anda akan kehilangan jejak tumpukan kronologis pengecualian jika Anda memiliki tipe pengembalian kosong pada metode async. Saya akan merekomendasikan mengembalikan Tugas sebagai berikut. Akan membuat debugging jauh lebih mudah.

public async Task DoFoo()
    {
        try
        {
            return await Foo();
        }
        catch (ProtocolException ex)
        {
            /* Exception with chronological stack trace */     
        }
    }
rohanjansen
sumber
Ini akan menyebabkan masalah dengan tidak semua jalur mengembalikan nilai, karena jika ada pengecualian tidak ada nilai yang dikembalikan, sementara di coba ada. Jika Anda tidak memiliki returnpernyataan, kode ini berfungsi, karena Task"secara implisit" dikembalikan dengan menggunakan async / await.
Matias Grioni
2

Blog ini menjelaskan masalah Anda Async Best Practices dengan rapi .

Intinya adalah Anda tidak boleh menggunakan void sebagai pengembalian untuk metode async, kecuali jika itu adalah event handler async, ini adalah praktik buruk karena tidak memungkinkan pengecualian ditangkap ;-).

Praktik terbaik adalah mengubah tipe pengembalian ke Tugas. Juga, coba kode async sepanjang jalan, membuat setiap panggilan metode async dan dipanggil dari metode async. Kecuali untuk metode Utama di konsol, yang tidak bisa async (sebelum C # 7.1).

Anda akan mengalami kebuntuan dengan aplikasi GUI dan ASP.NET jika Anda mengabaikan praktik terbaik ini. Kebuntuan terjadi karena aplikasi ini berjalan pada konteks yang hanya mengizinkan satu utas dan tidak akan menyerahkannya ke utas async. Ini berarti GUI menunggu secara sinkron untuk pengembalian, sementara metode async menunggu konteks: kebuntuan.

Perilaku ini tidak akan terjadi di aplikasi konsol, karena berjalan pada konteks dengan kumpulan utas. Metode async akan kembali pada utas lain yang akan dijadwalkan. Inilah sebabnya mengapa aplikasi konsol uji akan berfungsi, tetapi panggilan yang sama akan menemui jalan buntu di aplikasi lain ...

Stephan Ghequiere
sumber
1
"Kecuali untuk metode Utama di konsol, yang tidak boleh async." Sejak C # 7.1, Main sekarang bisa menjadi tautan
Adam