Podczas przechodzenia na nową platformę .NET Core 3 IAsynsDisposable
natknąłem się na następujący problem.
Rdzeń problemu: jeśli DisposeAsync
zgłasza wyjątek, wyjątek ten ukrywa wszelkie wyjątki await using
zgłoszone w bloku.
class Program
{
static async Task Main()
{
try
{
await using (var d = new D())
{
throw new ArgumentException("I'm inside using");
}
}
catch (Exception e)
{
Console.WriteLine(e.Message); // prints I'm inside dispose
}
}
}
class D : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
await Task.Delay(1);
throw new Exception("I'm inside dispose");
}
}
Tym, co zostaje złapane, jest AsyncDispose
wyjątek, jeśli zostanie rzucony, a wyjątek od wewnątrz await using
tylko, jeśli AsyncDispose
nie zostanie rzucony .
Wolałbym jednak odwrotnie: uzyskać wyjątek od await using
bloku, jeśli to możliwe, i- DisposeAsync
wyjątek tylko wtedy, gdy await using
blok zakończy się pomyślnie.
Uzasadnienie: Wyobraź sobie, że moja klasa D
współpracuje z niektórymi zasobami sieciowymi i subskrybuje niektóre powiadomienia zdalnie. Kod w środku await using
może zrobić coś złego i zawieść kanał komunikacyjny, po czym kod w Dispose, który próbuje z wdziękiem zamknąć komunikację (np. Wypisać się z powiadomień), również się nie powiedzie. Ale pierwszy wyjątek daje mi prawdziwe informacje o problemie, a drugi to tylko problem wtórny.
W innym przypadku, gdy główna część przebiegła, a usuwanie nie powiodło się, prawdziwy problem jest w środku DisposeAsync
, więc wyjątek od DisposeAsync
jest odpowiedni. Oznacza to, że zniesienie wszystkich wyjątków w środku DisposeAsync
nie powinno być dobrym pomysłem.
Wiem, że ten sam problem występuje w przypadku niesynchronicznym: wyjątek w finally
zastępuje wyjątek w try
, dlatego nie zaleca się wrzucania Dispose()
. Jednak w przypadku klas korzystających z dostępu do sieci tłumienie wyjątków w metodach zamykania wcale nie wygląda dobrze.
Możliwe jest obejście problemu za pomocą następującego pomocnika:
static class AsyncTools
{
public static async Task UsingAsync<T>(this T disposable, Func<T, Task> task)
where T : IAsyncDisposable
{
bool trySucceeded = false;
try
{
await task(disposable);
trySucceeded = true;
}
finally
{
if (trySucceeded)
await disposable.DisposeAsync();
else // must suppress exceptions
try { await disposable.DisposeAsync(); } catch { }
}
}
}
i używaj go jak
await new D().UsingAsync(d =>
{
throw new ArgumentException("I'm inside using");
});
co jest trochę brzydkie (i uniemożliwia takie rzeczy, jak wczesne powroty do bloku używającego).
Czy istnieje dobre kanoniczne rozwiązanie, await using
jeśli to możliwe? Moje wyszukiwanie w Internecie nie znalazło nawet dyskusji na ten temat.
Close
metodę z tego właśnie powodu. Prawdopodobnie rozsądnie jest zrobić to samo:CloseAsync
próbuje ładnie zamknąć rzeczy i powoduje porażkę.DisposeAsync
po prostu daje z siebie wszystko i po cichu zawodzi.CloseAsync
środki, muszę podjąć dodatkowe środki ostrożności, aby uruchomić. Jeśli po prostu umieszczę go na końcuusing
-block, zostanie on pominięty przy wczesnych powrotach itp. (Tak chcielibyśmy, aby się wydarzyło) i wyjątkach (tak chcielibyśmy, aby tak się stało). Ale pomysł wygląda obiecująco.Dispose
zawsze brzmiało: „Mogło być nie tak: po prostu staraj się poprawić sytuację, ale nie pogarszaj”, i nie rozumiem, dlaczegoAsyncDispose
powinno być inaczej.DisposeAsync
możliwe, aby posprzątać, ale nie rzucać, jest właściwym rozwiązaniem. Miałeś na myśli celowe wczesne powroty, w których celowe wczesne powroty mogłyby omyłkowo ominąć wezwanie doCloseAsync
: są to te, których zabrania wiele standardów kodowania.Odpowiedzi:
Są wyjątki, które chcesz ujawnić (przerwać bieżące żądanie lub sprowadzić proces), i są wyjątki, których oczekiwanie może się zdarzyć, i możesz sobie z nimi poradzić (np. Spróbuj ponownie i kontynuuj).
Ale rozróżnienie tych dwóch typów zależy od ostatecznego wywołującego kodu - jest to cały wyjątek, aby decyzja pozostawić osobie wywołującej.
Czasami program wywołujący przywiązuje większą wagę do ujawnienia wyjątku z oryginalnego bloku kodu, a czasem wyjątku od
Dispose
. Nie ma ogólnej zasady decydowania o tym, które powinny mieć pierwszeństwo. CLR jest co najmniej spójny (jak zauważyłeś) między zachowaniem synchronizacji a niesynchronizacją.Być może niefortunne jest to, że teraz musimy
AggregateException
reprezentować wiele wyjątków, więc nie można tego doposażyć do rozwiązania tego problemu. tzn. jeśli wyjątek jest już w locie, a inny zostanie zgłoszony, są one łączone wAggregateException
.catch
Mechanizm może zostać zmodyfikowany tak, że jeśli piszeszcatch (MyException)
to będzie złapać każdyAggregateException
, który zawiera wyjątek typuMyException
. Istnieje jednak wiele innych komplikacji wynikających z tego pomysłu i prawdopodobnie modyfikowanie czegoś tak fundamentalnego jest prawdopodobnie zbyt ryzykowne.Możesz ulepszyć swój system,
UsingAsync
aby wspierać wczesny zwrot wartości:źródło
await using
można użyć tylko standardu (w tym przypadku DisposeAsync nie rzuca się w przypadku niepowodującym śmierci), a pomocnik podobnyUsingAsync
jest bardziej odpowiedni (jeśli DisposeAsync może rzucić) ? (Oczywiście musiałbym zmodyfikować,UsingAsync
aby nie ślepo łapał wszystko, ale tylko nie śmiertelny (i bez kości w użyciu Erica Lipperta ).)Być może już rozumiesz, dlaczego tak się dzieje, ale warto to wyjaśnić. To zachowanie nie jest specyficzne dla
await using
. To by się stało z prostymusing
blokiem. Tak więc, kiedy mówięDispose()
tutaj, wszystko to dotyczyDisposeAsync()
również.using
Blok jest po prostu cukier syntaktyczny dlatry
/finally
bloku, jak sekcja Uwagi dokumentacji mówi. To, co widzisz, dzieje się, ponieważfinally
blok zawsze działa, nawet po wyjątku. Jeśli więc zdarzy się wyjątek i nie będziecatch
bloku, wyjątek zostanie wstrzymany do momentu uruchomieniafinally
bloku, a następnie wyjątek zostanie zgłoszony . Ale jeśli zdarzy się wyjątekfinally
, nigdy nie zobaczysz starego wyjątku.Możesz to zobaczyć na tym przykładzie:
Nie ma znaczenia, czy
Dispose()
czyDisposeAsync()
nazywa się wewnątrzfinally
. Zachowanie jest takie samo.Moja pierwsza myśl to: nie wrzucaj
Dispose()
. Ale po przejrzeniu części własnego kodu Microsoftu, myślę, że to zależy.Spójrz na
FileStream
przykład na ich wdrożenie . ZarównoDispose()
metoda synchroniczna , jak iDisposeAsync()
może generować wyjątki. SynchronicznaDispose()
nie ignorować pewne wyjątki celowo, ale nie wszystkie.Ale myślę, że ważne jest, aby wziąć pod uwagę charakter swojej klasy. Na
FileStream
przykład,Dispose()
opróżni bufor do systemu plików. To bardzo ważne zadanie i musisz wiedzieć, czy to się nie powiodło . Nie możesz tego po prostu zignorować.Jednak w innych typach obiektów, kiedy wywołujesz
Dispose()
, naprawdę nie ma już dla niego żadnego pożytku. WywołanieDispose()
naprawdę oznacza po prostu „ten obiekt jest dla mnie martwy”. Może to czyści część przydzielonej pamięci, ale niepowodzenie nie wpływa w żaden sposób na działanie aplikacji. W takim przypadku możesz zignorować wyjątek w swoim plikuDispose()
.Ale w każdym razie, jeśli chcesz rozróżnić wyjątek wewnątrz
using
lub wyjątek, który przyszedłDispose()
, potrzebujesztry
/catch
bloku zarówno wewnątrz, jak i na zewnątrzusing
bloku:Lub po prostu nie możesz użyć
using
. Wypisaćtry
/catch
/finally
blokować siebie, gdzie można złapać żadnego wyjątku wfinally
:źródło
catch
Wewnątrz wusing
bloku nie pomoże, ponieważ zwykle obsługi wyjątków odbywa się gdzieś daleko odusing
samego bloku, więc posługiwaniu się nim w środkuusing
nie jest zwykle bardzo możliwe. O używaniu nieusing
- czy to naprawdę lepsze niż proponowane obejście?try
/catch
/finally
block, ponieważ od razu byłoby jasne, co robi, bez konieczności czytania, coAsyncUsing
robi. Możesz także zachować możliwość wcześniejszego powrotu. Będzie także dodatkowy koszt procesoraAwaitUsing
. Byłby mały, ale tam jest.Dispose()
nie powinien rzucać, ponieważ jest wywoływany więcej niż jeden raz. Własne implementacje Microsoftu mogą generować wyjątki i nie bez powodu, jak pokazałem w tej odpowiedzi. Zgadzam się jednak, że powinieneś tego unikać, jeśli to w ogóle możliwe, ponieważ normalnie nikt nie spodziewałby się, że to rzuci.efektywne korzystanie z kodu obsługi wyjątków (cukier składniowy dla try ... wreszcie ... Dispose ()).
Jeśli kod obsługi wyjątków zgłasza wyjątki, coś po królewsku zostaje zepsute.
Cokolwiek jeszcze się zdarzyło, żeby cię tam dostać, tak naprawdę nie ma już znaczenia. Wadliwy kod obsługi wyjątków ukryje wszystkie możliwe wyjątki, w ten czy inny sposób. Kod obsługi wyjątków musi zostać naprawiony, co ma absolutny priorytet. Bez tego nigdy nie uzyskasz wystarczającej ilości danych do debugowania dla prawdziwego problemu. Widzę, że robi się to wyjątkowo źle. Łatwo się pomylić, tak samo jak posługiwanie się nagimi wskazówkami. Tak często istnieją dwa artykuły na temat I link, które mogą pomóc w wszelkich podstawowych nieporozumień projektowych:
W zależności od klasyfikacji wyjątków, musisz to zrobić, jeśli Twój kod obsługi wyjątku / upuszczenia zgłasza wyjątek:
W przypadku Fatal, Boneheaded i Vexing rozwiązanie jest takie samo.
Egzogenicznych wyjątków należy unikać nawet przy poważnych kosztach. Jest powód, dla którego nadal używamy plików dziennika , a nie baz danych do rejestrowania wyjątków - DB Opeartions to po prostu sposób na skłonność do wpadania w problemy egzogeniczne. Pliki dziennika to jedyny przypadek, w którym nawet nie mam nic przeciwko, jeśli zachowasz uchwyt pliku Otwórz cały środowisko wykonawcze.
Jeśli musisz zamknąć połączenie, nie przejmuj się zbytnio drugim końcem. Postępuj tak, jak robi to UDP: „Wyślę informacje, ale nie dbam o to, czy dostanie je druga strona”. Pozbywanie się polega na czyszczeniu zasobów po stronie klienta / stronie, nad którą pracujesz.
Mogę spróbować ich powiadomić. Ale sprzątanie rzeczy po stronie serwera / FS? To, co ich limity czasu i ich obsługa wyjątków jest odpowiedzialny.
źródło
NativeMethods.UnmapViewOfFile();
iNativeMethods.CloseHandle()
. Ale te są importowane z zewnątrz. Nie ma sprawdzania żadnej zwracanej wartości ani czegokolwiek innego, co mogłoby być użyte do uzyskania właściwego wyjątku .NET wokół wszystkiego, co może spotkać tych dwóch. Tak więc zdecydowanie się narzekam, SqlConnection.Dispose (bool) po prostu nie obchodzi. | Close jest o wiele ładniejszy, w rzeczywistości mówi serwerowi. Zanim zadzwoni, pozbądź się.Możesz spróbować użyć AggregateException i zmodyfikować kod w taki sposób:
https://docs.microsoft.com/ru-ru/dotnet/api/system.aggregateexception?view=netframework-4.8
https://docs.microsoft.com/ru-ru/dotnet/standard/parallel-programming/exception-handling-task-parallel-library
źródło