Czytałem więc pytanie o zmuszeniu modułu śmieciowego C # do uruchomienia tam, gdzie prawie każda odpowiedź jest taka sama: możesz to zrobić, ale nie powinieneś - z wyjątkiem bardzo rzadkich przypadków . Niestety nikt nie wyjaśnia, jakie są takie przypadki.
Czy możesz mi powiedzieć w jakim scenariuszu wymuszenie zbierania śmieci jest dobrym lub rozsądnym pomysłem?
Nie pytam o przypadki specyficzne dla C #, ale raczej wszystkie języki programowania, które mają moduł wyrzucania elementów bezużytecznych. Wiem, że nie możesz wymuszać GC na wszystkie języki, takie jak Java, ale załóżmy, że możesz.
Odpowiedzi:
Naprawdę nie można wydawać ogólnych oświadczeń o odpowiednim sposobie korzystania ze wszystkich implementacji GC. Różnią się bardzo. Więc porozmawiam z .NET, o którym pierwotnie mówiłeś.
Musisz dość dokładnie znać zachowanie GC, aby zrobić to z dowolnej logiki lub powodu.
Jedyną radą dotyczącą zbiórki, jaką mogę udzielić, jest: Nigdy tego nie rób.
Jeśli naprawdę znasz skomplikowane szczegóły GC, nie będziesz potrzebować mojej rady, więc to nie będzie miało znaczenia. Jeśli jeszcze nie wiesz ze 100% pewnością, to pomoże i będziesz musiał szukać w Internecie i znaleźć odpowiedź w następujący sposób: Nie powinieneś dzwonić do GC.Collect , lub alternatywnie: Powinieneś dowiedzieć się więcej o tym, jak działa GC wewnątrz i na zewnątrz, i dopiero wtedy poznasz odpowiedź .
Jest jedno bezpieczne miejsce, w którym warto używać GC .
GC.Collect to dostępny interfejs API, którego można używać do profilowania czasów rzeczy. Możesz profilować jeden algorytm, zbierać i profilować inny algorytm od razu, wiedząc, że GC pierwszego algo nie wystąpił podczas drugiego, wypaczając wyniki.
Tego rodzaju profilowanie to jedyny raz, który sugerowałbym każdemu ręcznie.
W każdym razie wymyślony przykład
Jednym z możliwych przypadków użycia jest to, że jeśli załadujesz naprawdę duże rzeczy, trafią one do Dużych Stert Obiektów, które trafią prosto do Gen 2, chociaż znowu Gen 2 jest dla obiektów o długiej żywotności, ponieważ gromadzi się rzadziej. Jeśli wiesz, że z jakiegoś powodu ładujesz krótkotrwałe obiekty do Gen 2, możesz je szybciej usunąć, aby zmniejszyć Gen 2 i szybciej zbierać kolekcje.
To najlepszy przykład, jaki mogłem wymyślić, i to nie jest dobre - presja LOH, którą tutaj budujesz, spowodowałaby częstsze kolekcje, a kolekcje są tak częste, jak to jest - są szanse, że wyczyści LOH tak samo jak tak szybko, jak zdmuchnąłeś go tymczasowymi przedmiotami. Po prostu nie ufam sobie, że zakładam lepszą częstotliwość zbierania niż sam GC - dostrojony przez ludzi o wiele mądrzejszych niż ja.
Porozmawiajmy więc o niektórych semantykach i mechanizmach w .NET GC ... lub ...
Wszystko, co myślę, że wiem o .NET GC
Każdy, kto znajdzie tutaj błędy - popraw mnie. Znaczna część GC jest znana jako czarna magia i chociaż próbowałem pominąć szczegóły, których nie byłem pewien, prawdopodobnie nadal coś popełniłem źle.
Poniżej celowo brakuje wielu szczegółów, których nie jestem pewien, a także znacznie większej ilości informacji, których po prostu nie jestem świadomy. Wykorzystaj te informacje na własne ryzyko.
Koncepcje GC
.NET GC występuje w niespójnych czasach, dlatego nazywa się go „niedeterministycznym”, co oznacza, że nie można polegać na nim w określonych momentach. Jest to również generator śmieci, co oznacza, że dzieli twoje obiekty na liczbę przejść GC, przez które przeszli.
Obiekty w stercie Gen 0 przeżyły 0 kolekcji, zostały one nowo utworzone, więc ostatnio nie wystąpiła żadna kolekcja od ich wystąpienia. Obiekty na stosie Gen 1 przeszły przez jeden przebieg kolekcji, podobnie obiekty na stosie Gen 2 przeżyły 2 przebiegi kolekcji.
Teraz warto zauważyć, dlaczego odpowiednio kwalifikuje te konkretne generacje i partycje. .NET GC rozpoznaje tylko te trzy generacje, ponieważ przebiegi kolekcji, które przechodzą przez te trzy stosy, są nieco inne. Niektóre obiekty mogą przetrwać zbiór przechodzi tysiące razy. GC pozostawia je po drugiej stronie partycji sterty Gen 2, nie ma sensu ich dalej dzielić, ponieważ tak naprawdę są Gen 44; przekazywanie kolekcji jest takie samo jak wszystko na stosie Gen 2.
Te pokolenia mają cele semantyczne, a także zaimplementowane mechanizmy, które je honorują, a do nich przejdę za chwilę.
Co jest w kolekcji
Podstawowa koncepcja przekazania kolekcji GC polega na tym, że sprawdza on każdy obiekt w przestrzeni sterty, aby sprawdzić, czy nadal istnieją aktywne odwołania (korzenie GC) do tych obiektów. Jeśli dla obiektu zostanie znaleziony katalog główny GC, oznacza to, że aktualnie wykonywany kod może nadal dotrzeć do tego obiektu i go użyć, więc nie można go usunąć. Jeśli jednak nie znaleziono katalogu głównego GC dla obiektu, oznacza to, że uruchomiony proces nie potrzebuje już obiektu, więc może go usunąć, aby zwolnić pamięć dla nowych obiektów.
Teraz, kiedy skończy sprzątać kilka obiektów i pozostawi niektóre z nich w spokoju, nastąpi niefortunny efekt uboczny: wolne przestrzenie między żywymi obiektami, w których martwe zostały usunięte. Ta fragmentacja pamięci, jeśli pozostawiona sama, po prostu zmarnowałaby pamięć, więc kolekcje zwykle wykonują tak zwane „zagęszczenie”, w którym zabiorą wszystkie żywe obiekty pozostawione i ściśną je razem na stosie, dzięki czemu wolna pamięć będzie sąsiadować po jednej stronie stosu dla Gen 0.
Biorąc teraz pod uwagę 3 stosy pamięci podzielone według liczby przejść, przez które przeszły, porozmawiajmy o tym, dlaczego takie partycje istnieją.
Kolekcja Gen 0
Gen 0, będąc absolutnie najnowszymi obiektami, zwykle jest bardzo mały - więc możesz go bezpiecznie zbierać bardzo często . Częstotliwość zapewnia, że sterty pozostają małe, a zbiory są bardzo szybkie, ponieważ gromadzą się na tak małej sterty. Opiera się to mniej więcej na heurystyce, która twierdzi: Znaczna większość tworzonych obiektów tymczasowych jest bardzo tymczasowa, więc tymczasowe nie będą już używane ani odwoływać się do nich prawie natychmiast po użyciu, a zatem mogą być gromadzone.
Kolekcja 1. generacji
Gen 1, będący obiektami, które nie należą do tej bardzo tymczasowej kategorii obiektów, może być raczej krótkotrwały, ponieważ nadal - ogromna część stworzonych obiektów nie jest używana długo. Dlatego Gen 1 zbiera również dość często, ponownie utrzymując niewielki stos, dzięki czemu zbiory są szybkie. Jednak zakłada się, że mniej obiektów jest tymczasowych niż Gen 0, więc zbiera się rzadziej niż Gen 0
Powiem szczerze, że nie znam mechanizmów technicznych, które różnią się między przepustką kolekcjonerską Gen 0 a Gen 1, jeśli są jakieś inne niż częstotliwość, którą zbierają.
Kolekcja Gen 2
Gen 2 teraz musi być matką wszystkich kup, prawda? Cóż, tak, mniej więcej tak. To miejsce, w którym żyją wszystkie twoje stałe obiekty - na przykład obiekt, w którym żyjesz
Main()
, i wszystko, do czego sięMain()
odwołuje, ponieważ będą one zakorzenione aż doMain()
powrotu po zakończeniu procesu.Biorąc pod uwagę, że Gen 2 jest zbiorem praktycznie wszystkiego, czego inne pokolenia nie mogły zebrać, jego obiekty są w dużej mierze trwałe lub przynajmniej żyją przynajmniej. Tak więc rozpoznanie bardzo niewielkiej części tego, co znajduje się w Gen 2, będzie w rzeczywistości czymś, co można zebrać, nie trzeba go często zbierać. Pozwala to również na spowolnienie gromadzenia, ponieważ wykonuje się o wiele rzadziej. Właśnie w tym miejscu zajęli się wszystkimi dodatkowymi zachowaniami dla nieparzystych scenariuszy, ponieważ mają czas na ich wykonanie.
Kupa dużych obiektów
Jednym z przykładów dodatkowych zachowań Gen 2 jest to, że wykonuje również kolekcję na stosie dużych obiektów. Do tej pory mówiłem całkowicie o stosie małych obiektów, ale środowisko wykonawcze .NET przydziela rzeczy o określonych rozmiarach do osobnej sterty z powodu tego, co nazwałem powyżej kompaktowaniem. Zagęszczanie wymaga przemieszczania obiektów po zakończeniu zbierania na stosie małych obiektów. Jeśli w Gen 1 znajduje się żywy obiekt o wielkości 10 MB, ukończenie zagęszczania po kolekcji zajmie znacznie więcej czasu, co spowolni kolekcję Gen 1. Tak więc obiekt 10 MB jest przydzielany do sterty dużych obiektów i zbierany podczas Gen 2, który tak rzadko się uruchamia.
Finalizacja
Innym przykładem są obiekty z finalizatorami. Umieszczasz finalizator na obiekcie, który odwołuje się do zasobów poza zakresem .NETs GC (zasoby niezarządzane). Finalizator to jedyny sposób, w jaki GC może zażądać gromadzenia niezarządzanego zasobu - zaimplementujesz go w celu ręcznego gromadzenia / usuwania / uwalniania niezarządzanego zasobu, aby upewnić się, że nie wycieknie on z twojego procesu. Kiedy GC może uruchomić finalizator obiektów, twoja implementacja wyczyści niezarządzany zasób, dzięki czemu GC będzie w stanie usunąć twój obiekt bez ryzyka wycieku zasobów.
Mechanizmem, dzięki któremu finalizatory to robią, jest bezpośrednie odwoływanie się do nich w kolejce finalizacji. Gdy środowisko wykonawcze przydziela obiekt za pomocą finalizatora, dodaje wskaźnik do tego obiektu do kolejki finalizacji i blokuje obiekt na miejscu (zwany pinowaniem), aby zagęszczenie go nie przesunęło, co spowodowałoby uszkodzenie odniesienia do kolejki finalizacji. W miarę upływu kolekcji, w końcu okaże się, że Twój obiekt nie ma już katalogu głównego GC, ale finalizacja musi zostać wykonana, zanim będzie można go zebrać. Kiedy obiekt jest martwy, kolekcja przeniesie swoje odwołanie z kolejki finalizacji i umieści odniesienie do niego w tak zwanej kolejce „FReachable”. Następnie kolekcja jest kontynuowana. W innym „niedeterministycznym” czasie w przyszłości osobny wątek znany jako wątek finalizatora przejdzie przez kolejkę FReachable, wykonując finalizatory dla każdego z wymienionych obiektów. Po zakończeniu kolejka FReachable jest pusta i odwróciła nieco w nagłówku każdego obiektu, co oznacza, że nie wymaga on finalizacji (ten bit można również odwrócić ręcznie za pomocą
GC.SuppressFinalize
co jest powszechne wDispose()
metodach), podejrzewam również, że odpiął obiekty, ale nie cytuj mnie w tym. Następna kolekcja, która pojawi się na stosie, w którym znajduje się ten obiekt, w końcu go zbierze. Kolekcje Gen 0 nawet nie zwracają uwagi na obiekty z tym bitem wymaganym do finalizacji, automatycznie je promuje, nawet nie sprawdzając ich rootowania. Nieukroszony obiekt, który wymaga finalizacji w Gen 1, zostanie rzucony doFReachable
kolejki, ale kolekcja nie robi z nim nic innego, więc żyje w Gen 2. W ten sposób wszystkie obiekty, które mają finalizator, i nieGC.SuppressFinalize
zostaną zebrane w Gen 2.źródło
Dam kilka przykładów. Podsumowując, rzadko zdarza się, że wymuszenie GC jest dobrym pomysłem, ale może być całkowicie tego warte. Ta odpowiedź pochodzi z mojego doświadczenia z literaturą .NET i GC. Powinien dobrze uogólniać na inne platformy (przynajmniej te, które mają znaczący GC).
Jeśli twoim celem jest przepustowość, tym rzadziej GC, tym lepiej. W takich przypadkach wymuszenie kolekcji nie może mieć pozytywnego wpływu (z wyjątkiem dość wymyślnych problemów, takich jak zwiększenie wykorzystania pamięci podręcznej procesora przez usunięcie martwych obiektów umieszczonych między nimi). Pobieranie partii jest bardziej wydajne dla wszystkich kolektorów, o których wiem. W przypadku aplikacji produkcyjnej używającej pamięci w stanie ustalonym indukowanie GC nie pomaga.
Podane powyżej przykłady dotyczą spójności i ograniczenia wykorzystania pamięci. W takich przypadkach indukowane GC mogą mieć sens.
Wydaje się, że istnieje szeroko rozpowszechniony pogląd, że GC jest boską istotą, która wywołuje zbiór, gdy jest to rzeczywiście optymalne. Żaden GC, o którym wiem, nie jest tak wyrafinowany i naprawdę bardzo trudno jest być optymalnym dla GC. GC wie mniej niż programista. Jego heurystyka oparta jest na licznikach pamięci i takich rzeczach, jak szybkość zbierania i tak dalej. Heurystyka jest zwykle dobra, ale nie rejestruje nagłych zmian w zachowaniu aplikacji, takich jak zwolnienie dużej ilości zarządzanej pamięci. Jest również ślepy na niezarządzane zasoby i wymagania dotyczące opóźnień.
Uwaga: koszty GC różnią się w zależności od wielkości sterty i liczby referencji na sterty. Na małej kupce koszt może być bardzo mały. Widziałem szybkości zbierania G2 z .NET 4.5 z 1-2 GB / s w aplikacji produkcyjnej o wielkości sterty 1 GB.
źródło
Zgodnie z ogólną zasadą śmieciarz będzie zbierał, gdy napotka „presję pamięci”, i dobrym pomysłem jest, aby nie zbierać go w innym czasie, ponieważ możesz spowodować problemy z wydajnością lub nawet zauważalne przerwy w wykonywaniu programu. I w rzeczywistości pierwszy punkt zależy od drugiego: przynajmniej dla pokoleniowego śmieciarza działa przynajmniej wydajniej, im wyższy jest stosunek śmieci do dobrych obiektów, więc w celu zminimalizowania czasu poświęcanego na wstrzymywanie programu , musi się odwlekać i pozwolić, aby śmieci gromadziły się jak najwięcej.
Odpowiednim czasem na ręczne wywołanie modułu wyrzucania elementów bezużytecznych jest wtedy, gdy skończysz robić coś, co 1) prawdopodobnie spowodowało powstanie dużej ilości śmieci, a 2) użytkownik oczekuje, że poświęci trochę czasu i pozostawi system bez odpowiedzi tak czy siak. Klasyczny przykład to koniec ładowania czegoś dużego (dokument, model, nowy poziom itp.)
źródło
Jedną rzeczą, o której nikt nie wspominał, jest to, że chociaż Windows GC jest niesamowicie dobry, GC na Xboksie jest śmieciem (gra słów zamierzona) .
Dlatego podczas kodowania gry XNA, która ma być uruchomiona na XBox, niezwykle ważne jest, aby zbieranie śmieci było odpowiednie dla odpowiednich chwil, w przeciwnym razie będziesz mieć okropne sporadyczne czkawki FPS. Ponadto w XBox często używa się go
struct
znacznie częściej niż zwykle, aby zminimalizować liczbę obiektów, które należy zebrać w śmietniku.źródło
Odśmiecanie jest przede wszystkim narzędziem do zarządzania pamięcią. W związku z tym śmieciarze będą zbierać, gdy pojawi się presja pamięci.
Nowoczesne śmieciarki są bardzo dobre i stają się coraz lepsze, więc jest mało prawdopodobne, że można je poprawić, zbierając ręcznie. Nawet jeśli dzisiaj możesz ulepszyć rzeczy, może się zdarzyć, że przyszłe ulepszenie wybranego śmieciarza sprawi, że Twoja optymalizacja będzie nieskuteczna, a nawet przyniesie efekt przeciwny do zamierzonego.
Jednak śmietniki zwykle nie próbują zoptymalizować wykorzystania zasobów innych niż pamięć. W środowiskach śmieciowych najbardziej wartościowe zasoby inne niż pamięć mają
close
metodę lub podobną metodę, ale w niektórych przypadkach nie jest to możliwe z jakiegoś powodu, na przykład zgodność z istniejącym interfejsem API.W takich przypadkach sensowne może być ręczne wywołanie odśmiecania, gdy wiadomo, że używany jest cenny zasób inny niż pamięć.
RMI
Jednym konkretnym przykładem tego jest zdalne wywołanie metody Java. RMI to zdalna biblioteka wywołań procedur. Zwykle masz serwer, który udostępnia różne obiekty do użytku przez klientów. Jeśli serwer wie, że obiekt nie jest używany przez żadnego klienta, wówczas obiekt ten kwalifikuje się do odśmiecania.
Jednak jedynym sposobem, w jaki serwer wie, jest to, jeśli klient mu to powie, a klient powie serwerowi, że nie potrzebuje już obiektu, gdy klient zbierze śmieci, cokolwiek z nich korzysta.
Stanowi to problem, ponieważ klient może mieć dużo wolnej pamięci, więc nie może zbyt często uruchamiać czyszczenia pamięci. Tymczasem serwer może mieć w pamięci wiele nieużywanych obiektów, których nie może gromadzić, ponieważ nie wie, że klient ich nie używa.
Rozwiązaniem w RMI jest okresowe uruchamianie śmiecia przez klienta, nawet gdy ma dużo wolnej pamięci, aby zapewnić szybkie gromadzenie obiektów na serwerze.
źródło
using
bloku lub w inny sposób wywołaćClose
metodę upewnij się, że zasób zostanie odrzucony jak najszybciej. Poleganie na GC w celu czyszczenia zasobów innych niż pamięć jest zawodne i powoduje różnego rodzaju problemy (szczególnie w przypadku plików, które muszą być zablokowane, aby można było je otworzyć tylko raz).close
metoda jest dostępna (lub zasób może być używany zusing
blokiem), jest to właściwe podejście. Odpowiedź dotyczy szczególnie rzadkich przypadków, w których mechanizmy te nie są dostępne.Najlepszą praktyką jest nie wymuszanie wyrzucania elementów bezużytecznych w większości przypadków. (Każdy system, nad którym pracowałem, wymuszał zbieranie śmieci, podkreślał problemy, które, jeśli rozwiązane, usunęłyby potrzebę wymuszania zbierania śmieci i znacznie przyspieszyły system).
Istnieje kilka przypadków , w których wiesz więcej na temat wykorzystania pamięci niż robi to moduł czyszczenia pamięci. Jest to mało prawdopodobne w przypadku aplikacji dla wielu użytkowników lub usługi, która odpowiada na więcej niż jedno żądanie na raz.
Jednak w niektórych procesach przetwarzania wsadowego wiesz więcej niż GC. Np. Rozważ aplikację, która.
Być może będziesz w stanie (po starannym) przetestować, czy powinieneś wymusić pełne wyrzucanie elementów bezużytecznych po przetworzeniu każdego pliku.
Kolejne przypadki to usługa, która budzi się co kilka minut w celu przetworzenia niektórych przedmiotów i nie utrzymuje żadnego stanu podczas snu . Następnie zmuszając pełną kolekcję tuż przed pójściem spać może się opłacać.
Wolę mieć interfejs API do wyrzucania elementów bezużytecznych, gdybym mógł dać mu wskazówki na temat tego typu rzeczy bez konieczności wymuszania na mnie GC.
Zobacz także „ Ciekawostki o wydajności Rico Mariani ”
źródło
Istnieje kilka przypadków, w których możesz chcieć samodzielnie wywołać gc ().
gc()
wywołaniu pozostanie bardzo niewiele obiektów, nie mówiąc już o przeniesieniu ich w przestrzeń starszej generacji ]. Kiedy zamierzasz stworzyć dużą kolekcję obiektów i zużyć dużo pamięci. Po prostu chcesz wyczyścić jak najwięcej miejsca, jak to możliwe. To tylko zdrowy rozsądek. Pogc()
ręcznym wywołaniu nie będzie zbędnego sprawdzania wykresu referencyjnego w części tej dużej kolekcji obiektów, które ładujesz do pamięci. Krótko mówiąc, jeśli uruchomiszgc()
przed załadowaniem dużo do pamięci,gc()
indukowane podczas ładowania zdarza się rzadziej co najmniej raz, gdy ładowanie rozpoczyna tworzenie ciśnienia pamięci.dużyobiekty, a raczej nie załadujesz więcej obiektów do pamięci. Krótko mówiąc, przechodzisz od fazy tworzenia do fazy używania. Dzwoniąc wgc()
zależności od implementacji, używana pamięć zostanie spakowana, co znacznie poprawia lokalizację pamięci podręcznej. Spowoduje to znaczną poprawę wydajności, której nie uzyskasz dzięki profilowaniu .gc()
a implementacja zarządzania pamięcią obsługuje, stworzysz znacznie lepszą ciągłość swojej pamięci fizycznej. To znowu sprawia, że nowa duża kolekcja obiektów jest bardziej ciągła i zwarta, co z kolei poprawia wydajnośćźródło
By calling gc() depending on implementation, the memory in used will be compacted which massively improves cache locality. This will result in massive improve in performance that you will not get from profiling.
Jeśli przydzielisz tonę obiektów z rzędu, są one już zagęszczone. Jeśli już, zbieranie śmieci może je nieco przetasować. Tak czy inaczej, użycie gęstszych struktur danych i nieprzeskakiwanie losowo w pamięci będzie miało większy wpływ. Jeśli używasz naiwnej listy połączonej jeden element na węzeł, to żadna ilość ręcznych sztuczek GC nie nadrobi tego.Przykład z prawdziwego świata:
Miałem aplikację internetową, która korzystała z bardzo dużego zestawu danych, które rzadko się zmieniały i do których trzeba było uzyskać dostęp bardzo szybko (wystarczająco szybko, aby uzyskać odpowiedź za naciśnięciem klawisza przez AJAX).
Rzeczą oczywistą jest załadowanie odpowiedniego wykresu do pamięci i dostęp do niego stamtąd, a nie do bazy danych, aktualizowanie wykresu po zmianie DB.
Ale ponieważ jest bardzo duży, naiwne obciążenie zajęłoby co najmniej 6 GB pamięci, a dane miałyby wzrosnąć w przyszłości. (Nie mam dokładnych danych, gdy stało się jasne, że mój komputer o pojemności 2 GB próbuje poradzić sobie z co najmniej 6 GB, miałem wszystkie pomiary, których potrzebowałem, aby wiedzieć, że to nie zadziała).
Na szczęście w tym zestawie danych znajdowała się duża liczba obiektów niezmiennych w popsicle, które były takie same; gdy już zorientowałem się, że pewna partia była taka sama jak inna partia, mogłem alias jednego odniesienia do drugiego, pozwalając na zebranie dużej ilości danych, a zatem zmieścić wszystko w mniej niż pół giganta.
Wszystko dobrze i dobrze, ale do tego wciąż przebija się ponad 6 GB obiektów w ciągu około pół minuty, aby dostać się do tego stanu. Pozostawiony sam sobie, GC nie poradził sobie; skok aktywności w stosunku do zwykłego schematu aplikacji (znacznie mniejszy przy zwolnieniach na sekundę) był zbyt ostry.
Okresowe dzwonienie
GC.Collect()
podczas procesu kompilacji oznaczało, że wszystko działało bezproblemowo. Oczywiście nie zadzwoniłem ręcznieGC.Collect()
przez resztę czasu działania aplikacji.Ten rzeczywisty przypadek jest dobrym przykładem wytycznych, kiedy powinniśmy użyć
GC.Collect()
:Przez większość czasu, gdy myślałem, że mogę mieć przypadek, w którym
GC.Collect()
warto zadzwonić, ponieważ zastosowanie miały punkty 1 i 2, punkt 3 sugerował, że pogorszyło to sytuację lub przynajmniej poprawiło sytuację (i przy niewielkiej lub żadnej poprawie skłaniaj się ku nieprzekazywaniu połączeń, ponieważ takie podejście może okazać się lepsze w trakcie życia aplikacji).źródło
Mam zastosowanie do usuwania śmieci, co jest nieco niekonwencjonalne.
Istnieje ta błędna praktyka, która jest niestety bardzo rozpowszechniona w świecie C #, polegająca na implementacji usuwania obiektów za pomocą brzydkiego, niezgrabnego, nieeleganckiego i podatnego na błędy idiomu znanego jako pozbywanie się IDisposable . MSDN opisuje to szczegółowo , a wiele osób przysięga na to, podąża za nim religijnie, spędza godziny na godziny, dyskutując dokładnie, jak należy to zrobić itp.
(Zwróć uwagę, że to, co nazywam tutaj brzydkim, nie jest samym wzorem usuwania obiektów; to, co nazywam brzydkim, to szczególny
IDisposable.Dispose( bool disposing )
idiom.)Ten idiom został wymyślony, ponieważ podobno niemożliwe jest zagwarantowanie, że niszczyciel twoich obiektów będzie zawsze wywoływany przez moduł wyrzucania elementów bezużytecznych w celu oczyszczenia zasobów, więc ludzie wykonują czyszczenie zasobów wewnątrz
IDisposable.Dispose()
, a na wypadek, gdyby zapomnieli, spróbują jeszcze raz w destruktorze. Wiesz, na wszelki wypadek.Ale wtedy możesz
IDisposable.Dispose()
mieć zarówno zarządzane, jak i niezarządzane obiekty do oczyszczenia, ale zarządzanych nie można wyczyścić, gdyIDisposable.Dispose()
jest wywoływany z poziomu destruktora, ponieważ zostały one już zajęte przez śmietnik w tym momencie, więc jest to potrzeba osobnejDispose()
metody, która akceptujebool disposing
flagę, aby wiedzieć, czy należy usuwać zarówno obiekty zarządzane, jak i niezarządzane, czy tylko te niezarządzane.Przepraszam, ale to jest po prostu szalone.
Opieram się na aksjomacie Einsteina, który mówi, że rzeczy powinny być tak proste, jak to możliwe, ale nie prostsze. Oczywiście nie możemy pominąć czyszczenia zasobów, więc najprostsze możliwe rozwiązanie musi obejmować przynajmniej to. Kolejne najprostsze rozwiązanie polega na tym, aby zawsze pozbywać się wszystkiego dokładnie w tym samym czasie, w którym ma on zostać unieszkodliwiony, bez komplikowania rzeczy, polegając na niszczycielu jako alternatywnym rozwiązaniu awaryjnym.
Teraz, ściśle mówiąc, jest to oczywiście niemożliwe, aby zagwarantować, że żaden programista nigdy nie popełnia błąd zapominając powołać
IDisposable.Dispose()
, ale to, co można zrobić, to użyć destruktora aby nadrobić ten błąd. To naprawdę bardzo proste: wszystko, co musi zrobić destruktor, to wygenerować wpis dziennika, jeśli wykryje, żedisposed
flaga obiektu jednorazowego nigdy nie była ustawionatrue
. Zatem użycie destruktora nie jest integralną częścią naszej strategii usuwania, ale jest naszym mechanizmem zapewnienia jakości. A ponieważ jest to test tylko w trybie debugowania, możemy umieścić cały destruktor w#if DEBUG
bloku, więc nigdy nie ponosimy żadnej kary za zniszczenie w środowisku produkcyjnym. (IDisposable.Dispose( bool disposing )
Idiom nakazuje toGC.SuppressFinalize()
należy wywoływać właśnie w celu zmniejszenia narzutu związanego z finalizacją, ale dzięki mojemu mechanizmowi można całkowicie uniknąć tego narzutu w środowisku produkcyjnym).Sprowadza się to do argumentu wiecznego twardego błędu vs. miękkiego błędu :
IDisposable.Dispose( bool disposing )
idiom jest podejściem opartym na miękkim błędzie i stanowi próbę umożliwienia programistom zapomnienia wywołaniaDispose()
bez awarii systemu, jeśli to możliwe. Metoda twardego błędu mówi, że programista musi zawsze upewnić się, żeDispose()
zostanie wywołany. Karą zwykle zalecaną przez podejście oparte na błędzie w większości przypadków jest niepowodzenie asercji, ale w tym konkretnym przypadku robimy wyjątek i zmniejszamy karę do zwykłego wydania wpisu dziennika błędów.Tak więc, aby ten mechanizm działał, wersja DEBUG naszej aplikacji musi wykonać pełne usuwanie śmieci przed zakończeniem, aby zagwarantować, że zostaną wywołane wszystkie destruktory, a tym samym złapać wszelkie
IDisposable
obiekty, o których zapomnieliśmy się pozbyć.źródło
Now, strictly speaking, it is of course impossible to guarantee that no programmer will ever make the mistake of forgetting to invoke IDisposable.Dispose()
W rzeczywistości tak nie jest, choć nie sądzę, że C # jest w stanie to zrobić. Nie narażaj zasobu; zamiast tego podaj DSL opisujący wszystko, co z nim zrobisz (w zasadzie monadę), a także funkcję, która pozyskuje zasób, robi rzeczy, uwalnia je i zwraca wynik. Sztuką jest użycie systemu typów, aby zagwarantować, że jeśli ktoś przemyci odniesienie do zasobu, nie będzie można go użyć w innym wywołaniu funkcji uruchamiania.Dispose(bool disposing)
(który nie jest zdefiniowany)IDisposable
polega na tym, że służy on do czyszczenia obiektów zarządzanych i niezarządzanych, które obiekt ma jako pole (lub w inny sposób jest za nie odpowiedzialny), co rozwiązuje niewłaściwy problem. niezarządzane obiekty w zarządzanym obiekcie bez innych obiektów jednorazowych, o które należy się martwić, wówczas wszystkieDispose()
metody będą albo jedną z tych metod (w razie potrzeby poproszę finalizator o takie samo czyszczenie), albo będą miały do dyspozycji tylko obiekty zarządzane (nie dysponujemy finalizatorem w ogóle), a potrzebabool disposing
znikniedispose(disposing)
tezą, że idiomem jest terribad, ale mówię tak, ponieważ ludzie tak często używają tej techniki i finalizatorów, gdy mają tylko zasoby zarządzane (DbConnection
na przykład obiekt jest zarządzany , nie jest zarządzany , nie jest połączony), a TYLKO POWINIENEŚ KAŻDY WDRAŻA FINALIZATOR Z NIEZARZĄDZANYM, PINVOKED, COM MARSHALLED LUB NIEBEZPIECZNYM KODEM . Opisałem powyżej w mojej odpowiedzi, jak bardzo drogie są finalizatory, nie używaj ich, chyba że masz niezarządzane zasoby w swojej klasie.dispose(dispoing)
uważa za podstawową ważną rzecz w tym idiomie, ale prawda jest taka, że jest to tak powszechne, ponieważ ludzie tak boją się materiałów GC, że coś tak niezwiązanego jak że (dispose
powinien mieć związek z GC) zasługuje na to, aby po prostu wziąć przepisany lek, nawet go nie badając. Dobrze, że go sprawdziłeś, ale tęskniłeś za największą całością (zachęca to bardziej finalistów farrr, niż powinni być)Mówiąc bardzo teoretycznie i nie uwzględniając problemów, takich jak niektóre implementacje GC spowalniające rzeczy podczas ich cykli zbierania, największym scenariuszem, jaki wymyślam, aby wymusić odśmiecanie, jest oprogramowanie o znaczeniu krytycznym, w którym logiczne wycieki są lepsze niż wiszące awarie wskaźnika, np. Z powodu awarii w nieoczekiwanych czasach może kosztować życie ludzkie lub coś w tym rodzaju.
Jeśli spojrzysz na niektóre gry typu shoddier indie napisane przy użyciu języków GC, takie jak gry Flash, wyciekają jak szalone, ale się nie zawieszają. Gra może zająć dziesięć razy więcej pamięci niż 20 minut, ponieważ część kodu w grze zapomniała ustawić wartość zerową lub usunąć ją z listy, a liczba klatek na sekundę może zacząć spadać, ale gra nadal działa. Podobna gra napisana przy użyciu tandetnego kodu C lub C ++ może ulec awarii w wyniku dostępu do wiszących wskaźników w wyniku tego samego rodzaju błędu zarządzania zasobami, ale nie wycieknie tak bardzo.
W przypadku gier awaria może być lepsza w tym sensie, że można ją szybko wykryć i naprawić, ale w przypadku programu o krytycznym znaczeniu, awaria w zupełnie nieoczekiwanych momentach może kogoś zabić. Myślę więc, że główne przypadki, w których nie występują awarie lub niektóre inne formy bezpieczeństwa są absolutnie niezbędne, a przeciek logiczny jest względnie trywialny.
Główny scenariusz, w którym myślę, że zmuszanie GC do złego działania jest złe, dotyczy rzeczy, w których logiczny wyciek jest w rzeczywistości mniej preferowany niż awaria. Na przykład w przypadku gier awaria nie musi nikogo zabić i może zostać łatwo złapana i naprawiona podczas testów wewnętrznych, podczas gdy logiczny wyciek może pozostać niezauważony nawet po wysłaniu produktu, chyba że jest tak poważny, że uniemożliwia grę w ciągu kilku minut . W niektórych domenach łatwiejsza do odtworzenia awaria występująca podczas testowania jest czasem lepsza niż wyciek, którego nikt nie zauważa natychmiast.
Innym przypadkiem, w którym mogę wymyślić wymuszenie GC w zespole, jest bardzo krótkotrwały program, taki jak po prostu wykonanie z wiersza poleceń, które wykonuje jedno zadanie, a następnie wyłącza się. W takim przypadku czas życia programu jest zbyt krótki, aby jakikolwiek logiczny wyciek nie był trywialny. Logiczne wycieki, nawet w przypadku dużych zasobów, zwykle stają się problematyczne dopiero kilka godzin lub minut po uruchomieniu oprogramowania, więc oprogramowanie, które ma być uruchamiane tylko przez 3 sekundy, prawdopodobnie nie będzie miało problemów z logicznymi przeciekami, i może mieć duże znaczenie prościej jest pisać tak krótkotrwałe programy, jeśli zespół użył GC.
źródło