Czy moja biblioteka zadań asynchronicznych powinna cicho połykać wyjątki?

10

Właśnie dowiedziałem się, że .NET 4.5 wprowadził zmianę w sposobie obsługi wyjątków wewnątrz Task. Mianowicie są cicho tłumione.

Oficjalne uzasadnienie, dlaczego tak się stało, brzmi: „chcieliśmy być bardziej przyjaźni dla niedoświadczonych programistów”:

W .NET 4.5 zadania mają znacznie większe znaczenie niż w .NET 4, ponieważ są one wprowadzone w językach C # i Visual Basic jako część nowych funkcji asynchronicznych obsługiwanych przez te języki. To w efekcie przenosi Zadania z domeny doświadczonych programistów do królestwa wszystkich. W rezultacie prowadzi to także do nowego zestawu kompromisów dotyczących tego, jak surowo obchodzić się z obsługą wyjątków.

( źródło )

Nauczyłem się ufać, że wiele decyzji w .NET było podejmowanych przez ludzi, którzy naprawdę wiedzą, co robią, i zwykle istnieje bardzo dobry powód, dla którego decydują. Ale ten mi ucieka.

Gdybym projektował własną bibliotekę zadań asynchronicznych, jaka jest zaleta przełykania wyjątków, których twórcy Framework zauważyli, że ich nie widzę?

Roman Starkov
źródło
4
+1. Dobre pytanie, biorąc pod uwagę fakt, że nie propagowanie wyjątków, z którymi nie można sobie poradzić, jest złą praktyką, nawet poza kontekstem asynchronicznym. Ponadto argument o niedoświadczonych programistach jest dość kiepski, IMHO. Co byłoby bardziej niezręczne dla początkujących niż fragment kodu, który nie tylko nie działa, ale także nie wprowadza żadnych wyjątków?
Arseni Mourzenko
@MainMa Dokładnie moje myśli, ale wcześniej się myliłem (kiedy okazało się, że w rzeczywistości miały bardzo dobry, ale nieoczywisty powód), więc pomyślałem, że zapytam.
Roman Starkov,
2
Wow, cieszę się, że opublikowałeś to tylko dlatego, że po raz pierwszy o tym słyszałem; i zdecydowanie narusza POLA . Masz rację, że ci faceci naprawdę wiedzą, co robią, więc jestem szczerze ciekawy, jakie może być tego uzasadnienie ... Zastanawiam się, czy ma to bardziej techniczny powód oparty na implementacji Async i jak propagowałyby wyjątki jako dno jest przekazywane do kortyny ..
Jimmy Hoffa

Odpowiedzi:

2

Jeśli chodzi o to, co warto, w dokumencie, do którego się odnosisz, podano przykładowe uzasadnienie:

Task op1 = FooAsync(); 
Task op2 = BarAsync(); 
await op1; 
await op2;

W tym kodzie programista uruchamia równolegle dwie asynchroniczne operacje, a następnie asynchronicznie czeka na każdą z nich, używając nowej funkcji języka oczekiwania ... [C] zastanawia się, co się stanie, jeśli wystąpią zarówno błędy op1, jak i op2. Oczekiwanie na op1 spowoduje propagację wyjątku op1, dlatego op2 nigdy nie będzie oczekiwany. W rezultacie wyjątek op2 nie będzie przestrzegany, a proces ostatecznie się zawiesi.

Aby ułatwić programistom pisanie kodu asynchronicznego na podstawie zadań, .NET 4.5 zmienia domyślne zachowanie wyjątków dla nieobserwowanych wyjątków. Chociaż nieobserwowane wyjątki nadal będą wywoływać zdarzenie UnobservedTaskException (nie zrobienie tego byłoby przełomową zmianą), proces domyślnie się nie zawiesi. Zamiast tego wyjątek zostanie zjedzony po wywołaniu zdarzenia, niezależnie od tego, czy moduł obsługi zdarzeń przestrzega wyjątku.

Nie przekonuje mnie to. Eliminuje możliwość jednoznacznego, ale trudnego do wyśledzenia błędu (tajemnicza awaria programu, która może wystąpić długo po rzeczywistym błędzie), ale zastępuje go możliwością całkowicie cichego błędu - który może stać się równie trudnym do wyśledzenia problemem później w twoim programie. To wydaje mi się wątpliwym wyborem.

Zachowanie jest konfigurowalne - ale oczywiście 99% programistów po prostu zastosuje zachowanie domyślne, nigdy nie myśląc o tym problemie. To, co wybrali jako domyślne, to wielka sprawa.


źródło
Ale zrobić dostać normalną wyjątek dla op1, prawda? Jeśli że jeden szczątki nieobsługiwany, to będzie obniżyć proces, prawda?
Timwi
1
@Timwi, jak rozumiem, ani op1wyjątek, ani op2wyjątek nie sprowadziłby programu. Zaletą jest to, że masz okazję obserwować oba. Ale jeśli tego nie zrobisz, oboje zostaną połknięci. Mogę się jednak mylić.
Jestem naprawdę zaskoczony, że uważają, że tak ważne jest, aby „obserwować” oba. Jeśli chcesz je „obserwować”, łapiesz je i zgłaszasz ich wystąpienie poprzez wynik zadania! ... Zgadzam się ze swoim rozumowaniem, +1
Roman Starkov
2

TL; DR - Nie , nie powinieneś po prostu cicho ignorować wyjątki.


Spójrzmy na założenia.

  • Wasza ślepa wiara

Nauczyłem się ufać, że wiele decyzji w .NET było podejmowanych przez ludzi, którzy naprawdę wiedzą, co robią, i zwykle istnieje bardzo dobry powód, dla którego decydują. Ale ten mi ucieka.

Bez wątpienia stwardnienie rozsiane ma naprawdę świetnych ludzi. Mają też wielu menedżerów i menedżerów, którzy są ... nieświadomi i konsekwentnie podejmują złe decyzje. O ile chcielibyśmy wierzyć w bajkę technicznie wytrawnego naukowca kierującego rozwojem produktu, rzeczywistość jest o wiele bardziej ponura.

Chodzi mi o to, że jeśli twój instynkt mówi ci, że coś może być nie tak, istnieje duża szansa (i wcześniejszy precedens), że jest to coś złego.

  • To jest problem techniczny

Sposób postępowania w przypadku wyjątków w tym przypadku jest decyzją dotyczącą czynników ludzkich, a nie decyzją techniczną. Luźno wyjątek jest błędem. Prezentacja błędu, nawet jeśli jest źle sformatowana, zapewnia użytkownikowi krytyczne informacje. Mianowicie, coś poszło nie tak.

Rozważ tę hipotetyczną rozmowę między użytkownikiem końcowym a deweloperem.

Użytkownik: Aplikacja jest zepsuta.
Dev: Co jest zepsute?
Użytkownik: Nie wiem, klikam przycisk i nic się nie dzieje.
Dev: Co masz na myśli mówiąc, że nic się nie dzieje?
Użytkownik: Słuchaj, klikam, czekam i czekam, i czekam i nic ... to oczywiście jest zepsute.

Nasz biedny, na szczęście hipotetyczny Deweloper musi teraz dowiedzieć się, co poszło nie tak w łańcuchu wydarzeń. Gdzie to było?
Powiadomienie modułu obsługi zdarzenia -> Procedura obsługi zdarzenia -> Metoda wywołana przez moduł obsługi -> początek asynchronicznego wywołania -> 7 warstw sieci OSI -> fizyczna transmisja -> wykonaj kopię zapasową 7 warstw sieci OSI -> usługa odbioru -> metoda wywołana przez usługę -> ... -> odpowiedź zwrotna wysłana przez usługę -> .... -> potwierdzenie asynch -> przetwarzanie odpowiedzi asynch -> ...

I pamiętaj, że opisałem kilka potencjalnych ścieżek błędów.


Po uwzględnieniu niektórych podstawowych założeń wydaje mi się, że staje się bardziej jasne, dlaczego ciche tłumienie wyjątków jest złym pomysłem. Wyjątek i powiązany komunikat o błędzie jest kluczowym wskaźnikiem dla użytkowników, że coś poszło nie tak. Nawet jeśli wiadomość nie ma znaczenia dla użytkownika końcowego, osadzone informacje mogą być przydatne dla programisty w zrozumieniu, co poszło nie tak. Jeśli będą mieli szczęście, wiadomość doprowadzi nawet do rozwiązania.

Myślę, że część problemu polega na tym, że niepoprawnie obsłużony wyjątek spowoduje awarię aplikacji. Pomijając wyjątki, aplikacja nie ulegnie awarii. Jest w tym jednak pewna ważność, ponieważ asynchronizacja polega na umożliwieniu aplikacji działania podczas pobierania danych. Logiczne rozszerzenie nie mówi, że jeśli możesz nadal działać podczas oczekiwania na wyniki, to niepowodzenie pobierania nie powinno również spowodować awarii aplikacji - wywołanie asynchroniczne jest domyślnie deklarowane jako niewystarczająco ważne, aby zakończyć aplikację.

Jest to rozwiązanie, ale rozwiązuje zły problem. Zapewnienie opakowania obsługi wyjątków nie wymaga tak dużego wysiłku, aby aplikacja mogła pozostać uruchomiona. Wychwycono wyjątek, komunikat o błędzie zostanie wyświetlony na ekranie lub zalogowany, a aplikacja będzie mogła nadal działać. Pominięcie wyjątku oznacza, że ​​ignorowanie błędów będzie A-OK, a dzięki testom upewnisz się, że wyjątki i tak nigdy nie uciekną w pole.

Ostatecznie oceniam, że jest to niedopracowana próba rozwiązania problemu stworzonego przez popchnięcie ludzi do modelu asynchronicznego, gdy nie byli gotowi na zmianę sposobu myślenia.


źródło