
Acara Pengecualian
Dalam kasus umum, kita tidak selalu tahu tentang pengecualian yang akan terjadi dalam program kita karena hampir selalu kita menggunakan sesuatu yang ditulis oleh orang lain dan yang ada di subsistem dan perpustakaan lain. Tidak hanya akan ada berbagai situasi dalam kode Anda sendiri, dalam kode perpustakaan lain, ada juga banyak masalah yang terkait dengan pelaksanaan kode di domain yang terisolasi. Dan hanya dalam kasus ini akan sangat berguna untuk dapat menerima data tentang pengoperasian kode yang terisolasi. Bagaimanapun, situasi mungkin cukup nyata ketika kode pihak ketiga mencegat semua kesalahan tanpa kecuali, menenggelamkan fault
mereka fault
blok:
try { // ... } catch { // do nothing, just to make code call more safe }
Dalam situasi seperti itu, eksekusi kode mungkin tidak lagi seaman kelihatannya, tetapi kami tidak memiliki pesan tentang masalah apa pun. Opsi kedua adalah ketika aplikasi menekan beberapa pengecualian, bahkan legal. Dan hasilnya - pengecualian berikut di tempat acak akan menyebabkan aplikasi crash di masa depan dari kesalahan yang tampaknya acak. Di sini saya ingin tahu apa latar belakang kesalahan ini. Apa jalannya acara menyebabkan situasi ini. Dan salah satu cara untuk membuat ini mungkin adalah dengan menggunakan acara tambahan yang berhubungan dengan situasi luar biasa: AppDomain.FirstChanceException
dan AppDomain.UnhandledException
.
Catatan
Bab yang diterbitkan di HabrΓ© tidak diperbarui dan, mungkin, sudah agak ketinggalan zaman. Dan karena itu, silakan buka teks asli untuk teks yang lebih baru:

Bahkan, ketika Anda "melempar pengecualian", metode biasa dari beberapa subsistem Throw
internal disebut, yang di dalamnya sendiri melakukan operasi berikut:
- Melemparkan
AppDomain.FirstChanceException
- Mencari filter yang cocok dalam rantai penangan
- Menyebabkan pawang memutar gulungan ke bingkai yang diinginkan.
- Jika tidak ada penangan yang ditemukan,
AppDomain.UnhandledException
, menabrak utas di mana pengecualian terjadi.
Seseorang harus segera membuat reservasi ketika menjawab pertanyaan yang menyiksa banyak pikiran: apakah mungkin untuk membatalkan pengecualian yang terjadi pada kode yang tidak terkontrol yang dijalankan dalam domain terisolasi tanpa dengan demikian memutus utas di mana pengecualian ini dilemparkan? Jawabannya singkat dan sederhana: tidak. Jika pengecualian tidak ditangkap pada seluruh rentang metode yang disebut, itu tidak bisa ditangani secara prinsip. Jika tidak, situasi aneh muncul: jika kita menggunakan AppDomain.FirstChanceException
menangani (semacam catch
sintetis) pengecualian, maka bingkai mana yang harus ditumpuk dengan tumpukan ulir? Bagaimana cara mengatur ini sebagai bagian dari aturan .NET CLR? Tidak mungkin. Itu tidak mungkin. Satu-satunya hal yang dapat kita lakukan adalah mencatat informasi yang diterima untuk penelitian selanjutnya.
Hal kedua untuk berbicara tentang "darat" adalah mengapa acara ini diperkenalkan bukan di Thread
, tetapi di AppDomain
. Lagi pula, jika Anda mengikuti logika, pengecualian muncul di mana? Dalam alur eksekusi perintah. Yaitu sebenarnya Thread
. Jadi mengapa domain memiliki masalah? Jawabannya sangat sederhana: untuk situasi apa AppDomain.FirstChanceException
dan AppDomain.UnhandledException
? Antara lain - untuk membuat kotak pasir untuk plugin. Yaitu untuk situasi ketika ada AppDomain
tertentu yang dikonfigurasi untuk PartialTrust. Apa pun bisa terjadi di dalam AppDomain ini: utas dapat dibuat di sana setiap saat, atau yang ada dari ThreadPool dapat digunakan. Kemudian ternyata kami, berada di luar proses ini (kami tidak menulis kode itu), kami tidak dapat berlangganan ke acara arus internal. Hanya karena kita tidak tahu aliran apa yang diciptakan di sana. Tetapi kami dijamin memiliki AppDomain
yang mengatur kotak pasir dan tautan yang kami miliki.
Jadi, pada kenyataannya, kita diberikan dua peristiwa regional: sesuatu terjadi yang tidak seharusnya ( FirstChanceExecption
) dan "semuanya buruk", tidak ada yang menangani pengecualian: itu tidak disediakan. Oleh karena itu, alur eksekusi perintah tidak masuk akal dan itu ( Thread
) akan dikirim.
Apa yang bisa diperoleh dengan memiliki acara-acara ini dan mengapa itu buruk bahwa pengembang memotong acara ini?
AppDomain.FirstChanceException
Acara ini pada dasarnya bersifat informasi dan tidak dapat "diproses". Tugasnya adalah untuk memberi tahu Anda bahwa pengecualian telah terjadi dalam domain ini dan akan mulai diproses oleh kode aplikasi setelah memproses acara tersebut. Eksekusinya membawa beberapa fitur yang harus diingat selama desain prosesor.
Tapi mari kita lihat contoh sintetik sederhana dari pemrosesan:
void Main() { var counter = 0; AppDomain.CurrentDomain.FirstChanceException += (_, args) => { Console.WriteLine(args.Exception.Message); if(++counter == 1) { throw new ArgumentOutOfRangeException(); } }; throw new Exception("Hello!"); }
Apa yang luar biasa dari kode ini? Di mana pun beberapa kode mengeluarkan pengecualian, hal pertama yang terjadi adalah mencatatnya ke konsol. Yaitu bahkan jika Anda lupa atau tidak bisa membayangkan menangani beberapa jenis pengecualian, itu akan tetap muncul di log peristiwa yang Anda kelola. Yang kedua adalah kondisi yang agak aneh untuk melempar pengecualian internal. Masalahnya adalah bahwa di dalam penangan FirstChanceException
Anda tidak bisa hanya melempar dan melempar satu pengecualian lagi. Sebaliknya, bahkan ini: di dalam penangan FirstChanceException, Anda tidak dapat melemparkan setidaknya pengecualian apa pun. Jika Anda melakukannya, ada dua kemungkinan acara. Pada awalnya, jika tidak ada kondisi if(++counter == 1)
, kita akan mendapatkan FirstChanceException
tanpa FirstChanceException
untuk ArgumentOutOfRangeException
semuanya baru. Apa artinya ini? Ini berarti bahwa pada tahap tertentu kita akan mendapatkan StackOverflowException
: throw new Exception("Hello!")
FirstChanceException
CLR Throw, yang melempar FirstChanceException
, yang melempar Throw
sudah untuk ArgumentOutOfRangeException
dan kemudian berulang. Opsi kedua - kami membela diri dengan kedalaman rekursi menggunakan kondisi counter
. Yaitu dalam hal ini, kami hanya melempar pengecualian satu kali. Hasilnya lebih dari yang tidak terduga: kami mendapatkan pengecualian yang benar-benar berfungsi di dalam instruksi Throw
. Dan apa yang paling cocok untuk jenis kesalahan ini? Menurut ECMA-335, jika sebuah instruksi dilemparkan ke dalam pengecualian, sebuah ExecutionEngineException
harus dibuang! Tetapi kami tidak dapat menangani situasi yang luar biasa ini. Ini menyebabkan crash lengkap dari aplikasi. Apa saja opsi pemrosesan aman yang kami miliki?
Hal pertama yang terlintas dalam pikiran adalah untuk mengatur blok try-catch
pada seluruh kode penangan FirstChanceException
:
void Main() { var fceStarted = false; var sync = new object(); EventHandler<FirstChanceExceptionEventArgs> handler; handler = new EventHandler<FirstChanceExceptionEventArgs>((_, args) => { lock (sync) { if (fceStarted) { // - , - , // try . Console.WriteLine($"FirstChanceException inside FirstChanceException ({args.Exception.GetType().FullName})"); return; } fceStarted = true; try { // . , Console.WriteLine(args.Exception.Message); throw new ArgumentOutOfRangeException(); } catch (Exception exception) { // Console.WriteLine("Success"); } finally { fceStarted = false; } } }); AppDomain.CurrentDomain.FirstChanceException += handler; try { throw new Exception("Hello!"); } finally { AppDomain.CurrentDomain.FirstChanceException -= handler; } } OUTPUT: Hello! Specified argument was out of the range of valid values. FirstChanceException inside FirstChanceException (System.ArgumentOutOfRangeException) Success !Exception: Hello!
Yaitu di satu sisi, kami memiliki kode untuk menangani acara FirstChanceException
, dan di sisi lain, kami memiliki kode tambahan untuk menangani pengecualian di FirstChanceException
itu sendiri. Namun, teknik logging untuk kedua situasi harus berbeda. Jika logging pemrosesan acara dapat berjalan sesuai keinginan Anda, maka pemrosesan kesalahan pemrosesan logika FirstChanceException
harus berjalan tanpa kecuali pada prinsipnya. Hal kedua yang mungkin Anda perhatikan adalah sinkronisasi antar utas. Ini mungkin menimbulkan pertanyaan: mengapa ada di sini jika ada pengecualian yang dilemparkan ke utas apa pun, yang berarti bahwa FirstChanceException
harus aman utas. Namun, semuanya tidak begitu ceria. FirstChanceException
kami miliki di AppDomain. Dan ini berarti bahwa itu terjadi untuk utas yang dimulai dalam domain tertentu. Yaitu jika kami memiliki domain tempat beberapa utas dimulai, maka FirstChanceException
dapat berjalan paralel. Dan ini berarti bahwa kita perlu melindungi diri kita dengan sinkronisasi: misalnya, menggunakan lock
.
Cara kedua adalah mencoba mengalihkan pemrosesan ke utas tetangga yang memiliki domain aplikasi berbeda. Namun, perlu disebutkan bahwa dengan implementasi seperti itu, kita harus membangun domain khusus untuk tugas ini sehingga tidak berfungsi sehingga aliran lain yang berfungsi dapat menempatkan domain ini:
static void Main() { using (ApplicationLogger.Go(AppDomain.CurrentDomain)) { throw new Exception("Hello!"); } } public class ApplicationLogger : MarshalByRefObject { ConcurrentQueue<Exception> queue = new ConcurrentQueue<Exception>(); CancellationTokenSource cancellation; ManualResetEvent @event; public void LogFCE(Exception message) { queue.Enqueue(message); } private void StartThread() { cancellation = new CancellationTokenSource(); @event = new ManualResetEvent(false); var thread = new Thread(() => { while (!cancellation.IsCancellationRequested) { if (queue.TryDequeue(out var exception)) { Console.WriteLine(exception.Message); } Thread.Yield(); } @event.Set(); }); thread.Start(); } private void StopAndWait() { cancellation.Cancel(); @event.WaitOne(); } public static IDisposable Go(AppDomain observable) { var dom = AppDomain.CreateDomain("ApplicationLogger", null, new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.BaseDirectory, }); var proxy = (ApplicationLogger)dom.CreateInstanceAndUnwrap(typeof(ApplicationLogger).Assembly.FullName, typeof(ApplicationLogger).FullName); proxy.StartThread(); var subscription = new EventHandler<FirstChanceExceptionEventArgs>((_, args) => { proxy.LogFCE(args.Exception); }); observable.FirstChanceException += subscription; return new Subscription(() => { observable.FirstChanceException -= subscription; proxy.StopAndWait(); }); } private class Subscription : IDisposable { Action act; public Subscription (Action act) { this.act = act; } public void Dispose() { act(); } } }
Dalam hal ini, penanganan FirstChanceException
seaman mungkin: di utas tetangga yang merupakan milik domain tetangga. Dalam hal ini, kesalahan dalam memproses pesan tidak dapat menurunkan alur kerja aplikasi. Plus, Anda dapat secara terpisah mendengarkan UnhandledException dari domain pencatatan pesan: kesalahan fatal selama pencatatan tidak akan menurunkan seluruh aplikasi.
AppDomain.UnhandledException
Pesan kedua yang bisa kami tangkap dan yang berhubungan dengan penanganan pengecualian adalah AppDomain.UnhandledException
. Pesan ini adalah berita yang sangat buruk bagi kami karena itu berarti bahwa tidak ada orang yang bisa menemukan cara untuk menangani kesalahan di utas tertentu. Juga, jika situasi seperti itu terjadi, yang bisa kita lakukan adalah "menghapus" konsekuensi dari kesalahan semacam itu. Yaitu dengan cara apa pun untuk membersihkan sumber daya yang hanya dimiliki oleh aliran ini jika ada yang dibuat. Namun, situasi yang lebih baik adalah menangani pengecualian saat berada di akar utas tanpa memblokir utas. Yaitu intinya menempatkan try-catch
. Mari kita coba mempertimbangkan kesesuaian perilaku ini.
Misalkan kita memiliki perpustakaan yang perlu membuat utas dan mengimplementasikan beberapa jenis logika dalam utas ini. Kami, sebagai pengguna perpustakaan ini, hanya tertarik untuk menjamin panggilan API serta menerima pesan kesalahan. Jika pustaka akan mogok aliran tanpa memberi tahu tentang hal itu, ini tidak dapat banyak membantu kami. Selain itu, runtuhnya aliran akan mengarah ke pesan AppDomain.UnhandledException
, di mana tidak ada informasi tentang aliran tertentu yang berada di sisinya. Jika kita berbicara tentang kode kita, aliran yang macet juga tidak akan berguna bagi kita. Bagaimanapun, saya tidak memenuhi kebutuhan untuk ini. Tugas kami adalah memproses kesalahan dengan benar, mengirim informasi tentang kemunculannya ke log kesalahan dan menghentikan aliran dengan benar. Yaitu pada dasarnya bungkus metode dengan mana thread dimulai pada try-catch
:
ThreadPool.QueueUserWorkitem(_ => { using(Disposables aggregator = ...){ try { // do work here, plus: aggregator.Add(subscriptions); aggregator.Add(dependantResources); } catch (Exception ex) { logger.Error(ex, "Unhandled exception"); } } });
Dalam skema seperti itu, kita mendapatkan apa yang kita butuhkan: di satu sisi, kita tidak akan merusak arus. Di sisi lain, bersihkan sumber daya lokal dengan benar jika dibuat. Nah, di lampiran - kami mengatur pencatatan kesalahan yang diterima. Tapi tunggu, katamu. Entah bagaimana Anda terkenal memunculkan masalah AppDomain.UnhandledException
event. Apakah ini benar-benar tidak perlu sama sekali? Itu perlu. Tetapi hanya untuk menginformasikan bahwa kami lupa untuk membungkus beberapa utas dalam try-catch
dengan semua logika yang diperlukan. Dengan segalanya: dengan penebangan dan pemurnian sumber daya. Kalau tidak, itu akan sepenuhnya salah: untuk mengambil dan memadamkan semua pengecualian, seolah-olah mereka tidak ada sama sekali.
Tautan ke seluruh buku
