Jawne usunięcie instancji obiektu lub wyczyszczenie powiązanych eventów

0

W mojej aplikacji trzymam sobie listę pewnych obiektów

public List<ExternalFileStorage> ExternalStorages { get; private set; }

mniejsza o to czym są te obiekty, ważne że wiążą one swoje metody z zewnętrznymi zdarzeniami (eventami). W tym przypadku uruchamia mi się funkcja Connection_BytesReceived jeśli ze źródła danych przyjdzie jakaś wiadomość.

public ExternalFileStorage(IExternalNode connection, ILogger logger)
        {
            _logger = logger;
            DataSource = connection;

            DataSource.BytesReceived += Connection_BytesReceived;
        }

ExternalStorage w pewnym momencie działania aplikacji zostaje "zresetowany". To znaczy usuwana jest obecna instancja z listy i generowana jest nowa

private void Node_ConnectionReset(object sender, ConnectionResetEventArgs e)
        {
            ExternalStorage relatedStorage = ExternalStorages.FirstOrDefault(x => x.DeviceId == e.RelatedNode.Id);
            if (relatedStorage != null)
            {
                // tutaj usuwam obecny "external storage"
                ExternalStorages.Remove(relatedStorage);
                // tutaj tworzę nowy, powiązany z tym samym źródłem danych
                CreateStorageForNode(e.RelatedNode);
            }
            else
            {
                _logger.LogError("ConnectionReset: couldn't refresh storage as it doesn't exist");
            }
        }

Zauważyłem, że kod w Connection_BytesReceived (tego w ExternalStorage, który wywołuje się po nadejściu wiadomości ze źródła danych) wywołuje się wielokrotnie, im więcej resetów tym więcej razy jest wykonywany. Moja teoria jest taka, że usuwany z listy ExternalStorage nadal znajduje się w pamięci. Normalnie byłby usunięty ale garbage collector go nie tyka, bo ExternalStorage nadal jest powiązany eventami z innymi obiektami. Gdy więc przychodzi wiadomość ze źródła, wywoływany jest Connection_BytesReceived nawet w tych obiektach, które rzekomo zostały już usunięte.

Problem polega na tym, że nie widzę w c# jawnego sposobu wywołania destruktora. Moje pytanie brzmi: czy jest jakiś sposób żeby definitywnie pozbyć się obiektu z pamięci żeby uniknąć takich sytuacji?

1

Nie znam kodu Connection_BytesReceived, ale zgaduję, ze tam przypinasz ExternalFileStorage do DataSource, skutkiem czego nie może być nigdy usunięty z pamięci.
Usuń wszystkie referencje, także te z eventów i będzie działać. Destruktor do tego niepotrzebny.

0
somekind napisał(a):

Nie znam kodu Connection_BytesReceived, ale zgaduję, ze tam przypinasz ExternalFileStorage do DataSource, skutkiem czego nie może być nigdy usunięty z pamięci.

Co znaczy "przypinam"? W przywołanej przez ciebie funkcji DataSource występuje tylko gdy wywołuję jakieś jego metody. Natomiast przypięcie do eventu następuje w konstruktorze klasy ExternalFileStorage który wkleiłem w poście wyżej.

somekind napisał(a):

Usuń wszystkie referencje, także te z eventów i będzie działać. Destruktor do tego niepotrzebny.

Z tym że wydaje mi się to dość ryzykowne rozwiązanie. W momencie gdy tych eventów czy referencji będzie przybywać, problem pojawi się znowu i za każdym razem będę musiał pamiętać żeby wszystko posprzątać i garbage collector mógł obiekt usunąć.

1
iteredi napisał(a):

Co znaczy "przypinam"? W przywołanej przez ciebie funkcji DataSource występuje tylko gdy wywołuję jakieś jego metody. Natomiast przypięcie do eventu następuje w konstruktorze klasy ExternalFileStorage który wkleiłem w poście wyżej.

Connection_BytesReceived znajduje się w ExternalFileStorage, co oznacza, że DataSource trzyma referencję do obiektu tej klasy, więc nie zostanie usunięty, dopóki obiekt DataSource istnieje.

Z tym że wydaje mi się to dość ryzykowne rozwiązanie. W momencie gdy tych eventów czy referencji będzie przybywać, problem pojawi się znowu i za każdym razem będę musiał pamiętać żeby wszystko posprzątać i garbage collector mógł obiekt usunąć.

No skoro to jest "ryzykowne rozwiązanie", bo "musisz pamiętać", to zostaje Ci Process.GetCurrentProcess().Kill().
Albo najlepiej przejdź na C/C++, tam będziesz mógł sobie sprzątać wszystko znacznie wygodniej i żaden GC Cię nie powstrzyma. ;)

2
iteredi napisał(a):

Problem polega na tym, że nie widzę w c# jawnego sposobu wywołania destruktora. Moje pytanie brzmi: czy jest jakiś sposób żeby definitywnie pozbyć się obiektu z pamięci żeby uniknąć takich sytuacji?

Nie, to nie jest Twój problem. Jak to sobie w ogóle wyobrażasz. Dla uproszczenia przyjmijmy że nie subskrybujemy eventów tylko normalnie przypisujemy instancję obserwatora:

class TwojaKlasa
{
    public void CosSieStalo() {}
}

class KlasaPowiadamiajaca
{
   public TwojaKlasa KtoChceWiedziec; // odpowiednik eventa BytesReceived
   public void CosSieStalo()
   {
       KtoChceWiedziec.CosSieStalo();
   }
}

subskrybując event, dodajesz instancję twojej klasy do listy obserwatorów - tu dla uproszczenia nie mamy listy tylko zwykłą zmienną

KlasaPowiadamiajaca p = new KlasaPowiadamiajaca();
TwojaKlasa k = new TwojaKlasa();
p.KtoChceWiedziec = k; // odpowiednik p.BytesReceived += k.Connection_BytesReceived;

teraz jak widzisz w klasie "KlasaPowiadamiajaca" masz odnośnik do twojej klasy. Powiedz mi - co nieszczęsny CLR miałby zrobić gdy chciałbyś "ostatecznie usunąć" instancję "k" bez usunięcia instancji "p"? Skąd miałby wiedzieć co ma zrobić w klasie powiadamiającej żeby dalej działała ale nie miała już odniesienia do twojej klasy?
GC musiałby przejść po wszystkich obiektach i usunąć z nich referencje - a co z polami "readonly"? Zapewne aplikacja zaraz by się wywaliła w losowym miejscu krzycząc o NullReferenceException bo ktoś nagle im usunął obiekty z gardła.

Tak jak wspomniano wyżej - musisz ręcznie odpiąć instancje "k" od instancji "p"

Dodatkowym rozwiązaniem na Twoje problemy mogą być WeakEventy:
https://docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/weak-event-patterns
https://www.codeproject.com/Articles/738109/The-NET-Weak-Event-Pattern-in-Csharp
Dzięki nim nie musisz się martwić o odpinanie eventów, a Twój istniejący kod będzie działał prawie tak jak sobie wyobrażasz bez wycieków pamięci. Nadal nie będziesz panował nad konkretnym momentem w którym obiekt jest usunięty, a więc do najbliższego odpalenia GC, nadal może reagować na eventy, z tego powodu warto go zastopować ręcznie.

W skrócie - żeby nie zniszczyć innych klas, musisz się pozbyć wszystkich twardych referencji, możesz sobie trzymać dowolną ilość WeakReference ale trzeba być ostrożnym przy zarządzaniu nimi.

iteredi napisał(a):

Z tym że wydaje mi się to dość ryzykowne rozwiązanie. W momencie gdy tych eventów czy referencji będzie przybywać, problem pojawi się znowu i za każdym razem będę musiał pamiętać żeby wszystko posprzątać i garbage collector mógł obiekt usunąć.

brzmi jakbyś niezbyt panował nad swoim kodem. Jak subskrybujesz jakiś event to powinieneś go też odsubskrybować. Zazwyczaj robi się to w metodzie Dispose()

Lektura dla Ciebie pokrywająca właściwie wszystko to co napisałem wcześniej:
https://michaelscodingspot.com/5-techniques-to-avoid-memory-leaks-by-events-in-c-net-you-should-know/

1

Do eventów w C# można się subskrybować, ale można też usuwać subskrypcje. Ważne jest żeby użyć dokładnie tego samego obiektu który był użyty do subskrypcji:

DataSource.BytesReceived += Connection_BytesReceived;

// Unsubscribe
DataSource.BytesReceived -= Connection_BytesReceived;

Jeżeli zamiast referencji do metody byłby new DelegateType(Method) to by już nie zadziałało z -=.

Wystarczy że unsubskrypcje umieścisz w Dispose i po usunięciu elementów z listy będziesz je Dispose'ował. Generalnie nie odpięcie się od zdarzenia to no. 1 przyczyna wycieków pamięci w C#.

1 użytkowników online, w tym zalogowanych: 0, gości: 1