Dlaczego i jak uniknąć wycieków pamięci programu Event Handler?

154

Właśnie zdałem sobie sprawę, czytając kilka pytań i odpowiedzi w StackOverflow, że dodanie programów obsługi zdarzeń +=w C # (lub, jak sądzę, innych językach .net) może powodować typowe wycieki pamięci ...

Wiele razy korzystałem z takich programów obsługi zdarzeń i nigdy nie zdawałem sobie sprawy, że mogą one powodować lub były przyczyną wycieków pamięci w moich aplikacjach.

Jak to działa (czyli dlaczego faktycznie powoduje to wyciek pamięci)?
Jak mogę rozwiązać ten problem? Czy -=wystarczy użyć tego samego programu obsługi zdarzeń?
Czy istnieją wspólne wzorce projektowe lub najlepsze praktyki dotyczące postępowania w takich sytuacjach?
Przykład: Jak mam obsługiwać aplikację, która ma wiele różnych wątków, używając wielu różnych programów obsługi zdarzeń do zgłaszania kilku zdarzeń w interfejsie użytkownika?

Czy są jakieś dobre i proste sposoby efektywnego monitorowania tego w już zbudowanej dużej aplikacji?

gillyb
źródło

Odpowiedzi:

188

Przyczyna jest prosta do wyjaśnienia: podczas gdy program obsługi zdarzeń jest subskrybowany, wydawca zdarzenia przechowuje odwołanie do subskrybenta za pośrednictwem delegata programu obsługi zdarzeń (zakładając, że delegat jest metodą instancji).

Jeśli wydawca żyje dłużej niż subskrybent, to utrzyma go przy życiu nawet wtedy, gdy nie ma innych odniesień do subskrybenta.

Jeśli zrezygnujesz z subskrypcji zdarzenia za pomocą równej obsługi, to tak, to usunie procedurę obsługi i możliwy wyciek. Jednak z mojego doświadczenia wynika, że ​​rzadko stanowi to problem - ponieważ zazwyczaj wydaje mi się, że i tak wydawca i subskrybent mają mniej więcej równe życie.

Jest to możliwa przyczyna ... ale z mojego doświadczenia wynika, że ​​jest to raczej przesadzone. Oczywiście Twój przebieg może się różnić ... po prostu musisz być ostrożny.

Jon Skeet
źródło
... Widziałem kilka osób piszących o tym, odpowiadając na pytania typu „jakie są najczęstsze wycieki pamięci w .net”.
gillyb
32
Sposobem na obejście tego od strony wydawcy jest ustawienie zdarzenia na zero, gdy masz pewność, że nie będziesz go więcej uruchamiać. Spowoduje to niejawne usunięcie wszystkich subskrybentów i może być przydatne, gdy pewne zdarzenia są uruchamiane tylko na określonych etapach życia obiektu.
JSB
2
Metoda Dipose byłaby dobrym momentem na
zerwanie
6
@DaviFiamenghi: Cóż, jeśli coś jest usuwane, jest to przynajmniej prawdopodobna wskazówka, że ​​wkrótce będzie się kwalifikować do zbierania śmieci, w którym momencie nie ma znaczenia, jacy są subskrybenci.
Jon Skeet
1
@ BrainSlugs83: „a typowy wzorzec zdarzenia i tak zawiera nadawcę” - tak, ale to jest producent zdarzenia . Zazwyczaj instancja subskrybenta zdarzenia jest istotna, a nadawca nie. Więc tak, jeśli możesz subskrybować za pomocą metody statycznej, nie stanowi to problemu - ale z mojego doświadczenia rzadko jest to opcja.
Jon Skeet
13

Tak, -=wystarczy, jednak śledzenie każdego przydzielonego zdarzenia może być dość trudne. (szczegóły w poście Jona). Jeśli chodzi o wzorzec projektowy, spójrz na słaby wzorzec zdarzeń .

Femaref
źródło
Jeśli wiem, że wydawca będzie żył dłużej niż subskrybent, dokonuję subskrypcji IDisposablei wypisuję się z wydarzenia.
Shimmy Weitzhandler
9

Wyjaśniłem to zamieszanie na blogu pod adresem https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16 . Postaram się to podsumować tutaj, abyś miał jasny pomysł.

Odniesienie oznacza „Potrzeba”:

Przede wszystkim musisz zrozumieć, że jeśli obiekt A zawiera odniesienie do obiektu B, to będzie to oznaczać, że obiekt A potrzebuje obiektu B do działania, prawda? Więc odśmiecacz nie zbierze obiektu B tak długo, jak długo obiekt A będzie żył w pamięci.

Myślę, że ta część powinna być oczywista dla programisty.

+ = Oznacza, wstawianie odniesienia obiektu po prawej stronie do obiektu po lewej stronie:

Ale zamieszanie pochodzi od operatora C # + =. Ten operator nie mówi jasno programiście, że prawa strona tego operatora w rzeczywistości wstrzykuje odniesienie do obiektu po lewej stronie.

wprowadź opis obrazu tutaj

Robiąc to, obiekt A myśli, że potrzebuje przedmiotu B, chociaż z twojej perspektywy obiekt A nie powinien przejmować się tym, czy obiekt B żyje, czy nie. Ponieważ obiekt A uważa, że ​​obiekt B jest potrzebny, obiekt A chroni obiekt B przed wyrzucaniem elementów bezużytecznych, dopóki obiekt A żyje. Ale jeśli nie chcesz, aby ochrona została udzielona obiektowi subskrybenta zdarzenia, możesz powiedzieć, że wystąpił wyciek pamięci.

wprowadź opis obrazu tutaj

Możesz uniknąć takiego wycieku, odłączając moduł obsługi zdarzeń.

Jak podjąć decyzję?

Ale istnieje wiele zdarzeń i programów obsługi zdarzeń w całej bazie kodu. Czy to oznacza, że ​​musisz wszędzie odłączać programy obsługi zdarzeń? Odpowiedź brzmi: nie. Gdybyś musiał to zrobić, twój kod źródłowy byłby naprawdę brzydki z gadatliwością.

Możesz raczej postępować zgodnie z prostym schematem blokowym, aby określić, czy odłączająca procedura obsługi zdarzeń jest konieczna, czy nie.

wprowadź opis obrazu tutaj

W większości przypadków może się okazać, że obiekt subskrybenta zdarzenia jest tak samo ważny jak obiekt wydawcy zdarzenia i oba mają istnieć w tym samym czasie.

Przykład scenariusza, w którym nie musisz się martwić

Na przykład zdarzenie kliknięcia przycisku w oknie.

wprowadź opis obrazu tutaj

W tym przypadku wydawcą zdarzenia jest Button, a subskrybentem zdarzenia jest MainWindow. Stosując ten schemat blokowy, zadaj pytanie, czy okno główne (subskrybent zdarzenia) ma być martwe przed przyciskiem (wydawcą zdarzenia)? Oczywiście nie, prawda? To nawet nie ma sensu. Po co więc martwić się odłączaniem modułu obsługi zdarzeń kliknięcia?

Przykład, gdy odłączenie programu obsługi zdarzeń jest MUSI.

Podam jeden przykład, w którym obiekt subskrybenta powinien być martwy przed obiektem wydawcy. Powiedzmy, że Twoje MainWindow publikuje zdarzenie o nazwie „SomethingHappened” i po kliknięciu przycisku wyświetlasz okno potomne z okna głównego. Okno potomne subskrybuje to zdarzenie okna głównego.

wprowadź opis obrazu tutaj

Okno potomne subskrybuje zdarzenie w oknie głównym.

wprowadź opis obrazu tutaj

Na podstawie tego kodu możemy jasno zrozumieć, że w głównym oknie znajduje się przycisk. Kliknięcie tego przycisku pokazuje okno potomne. Okno potomne nasłuchuje zdarzenia z okna głównego. Po wykonaniu czynności użytkownik zamyka okno potomne.

Teraz, zgodnie ze schematem blokowym, który podałem, jeśli zadasz pytanie „Czy okno podrzędne (subskrybent zdarzenia) powinno być martwe przed wydawcą zdarzenia (okno główne)? Odpowiedź powinna brzmieć TAK. Zgadza się? Więc odłącz moduł obsługi zdarzenia Zwykle robię to ze zdarzenia Unloaded w oknie.

Praktyczna zasada: jeśli widok (tj. WPF, WinForm, UWP, Xamarin Form itp.) Subskrybuje zdarzenie ViewModel, zawsze pamiętaj o odłączeniu obsługi zdarzeń. Ponieważ ViewModel zwykle trwa dłużej niż widok. Tak więc, jeśli ViewModel nie zostanie zniszczony, każdy widok, który zasubskrybował zdarzenie tego ViewModel, pozostanie w pamięci, co nie jest dobre.

Dowód koncepcji przy użyciu profilera pamięci.

Nie będzie zabawnie, jeśli nie możemy zweryfikować koncepcji za pomocą profilera pamięci. W tym eksperymencie użyłem JetBrain dotMemory profilera.

Najpierw uruchomiłem MainWindow, które wygląda tak:

wprowadź opis obrazu tutaj

Następnie zrobiłem migawkę pamięci. Następnie trzykrotnie kliknąłem przycisk . Pojawiły się trzy okna potomne. Zamknąłem wszystkie te okna podrzędne i kliknąłem przycisk Force GC w profilerze dotMemory, aby upewnić się, że wywoływany jest Garbage Collector. Następnie zrobiłem kolejną migawkę pamięci i porównałem ją. Ujrzeć! nasz strach był prawdziwy. Okno potomne nie zostało zebrane przez moduł wyrzucania elementów bezużytecznych nawet po zamknięciu. Co więcej, liczba wycieków obiektu ChildWindow jest również wyświetlana jako „ 3 ” (kliknąłem przycisk 3 razy, aby wyświetlić 3 okna potomne).

wprowadź opis obrazu tutaj

W takim razie odłączyłem procedurę obsługi zdarzeń, jak pokazano poniżej.

wprowadź opis obrazu tutaj

Następnie wykonałem te same czynności i sprawdziłem profiler pamięci. Tym razem wow! koniec wycieku pamięci.

wprowadź opis obrazu tutaj

Emran Hussain
źródło
3

Zdarzenie to tak naprawdę połączona lista programów obsługi zdarzeń

Kiedy robisz + = new EventHandler na zdarzeniu, nie ma znaczenia, czy ta konkretna funkcja została wcześniej dodana jako detektor, zostanie dodana raz na + =.

Gdy zdarzenie jest wywoływane, przechodzi przez połączoną listę, pozycja po pozycji i wywołuje wszystkie metody (programy obsługi zdarzeń) dodane do tej listy, dlatego programy obsługi zdarzeń są nadal wywoływane, nawet jeśli strony nie są już uruchomione, o ile są są żywe (zakorzenione) i będą żyły tak długo, jak długo będą połączone. Będą więc wywoływane, dopóki eventhandler nie zostanie odłączony za pomocą - = new EventHandler.

Spójrz tutaj

i MSDN TUTAJ

TalentTuner
źródło