Czy Garbage Collector zadzwoni do IDisposable.Dispose dla mnie?

134

Wzorzec .NET IDisposable oznacza, że jeśli napiszesz finalizator i zaimplementujesz IDisposable, finalizator musi jawnie wywołać Dispose. Jest to logiczne i zawsze to robiłem w rzadkich sytuacjach, gdy finalizator jest uzasadniony.

Jednak co się stanie, jeśli po prostu to zrobię:

class Foo : IDisposable
{
     public void Dispose(){ CloseSomeHandle(); }
}

i nie implementuj finalizatora ani nic takiego. Czy struktura wywoła za mnie metodę Dispose?

Tak, zdaję sobie sprawę, że to brzmi głupio i cała logika sugeruje, że tak nie będzie, ale zawsze miałem dwie rzeczy z tyłu głowy, które sprawiały, że nie byłem pewien.

  1. Ktoś kilka lat temu powiedział mi kiedyś, że rzeczywiście tak się stanie, a ta osoba miała bardzo solidne doświadczenie w „znajomości swoich rzeczy”.

  2. Kompilator / framework wykonuje inne „magiczne” rzeczy w zależności od tego, jakie interfejsy zaimplementujesz (np. Foreach, metody rozszerzające, serializacja oparta na atrybutach itp.), Więc ma sens, że może to być również „magiczne”.

Chociaż czytałem wiele rzeczy na ten temat i było wiele sugestii, nigdy nie byłem w stanie znaleźć ostatecznej odpowiedzi Tak lub Nie na to pytanie.

Orion Edwards
źródło

Odpowiedzi:

121

.Net Garbage Collector wywołuje metodę Object.Finalize obiektu podczas czyszczenia pamięci. Przez domyślnie to robi nic i musi być overidden jeśli chcesz uwolnić dodatkowe zasoby.

Dispose NIE jest wywoływane automatycznie i musi być wywoływane wprost, jeśli zasoby mają zostać zwolnione, na przykład w ramach bloku „using” lub „try last”

zobacz http://msdn.microsoft.com/en-us/library/system.object.finalize.aspx, aby uzyskać więcej informacji

Xian
źródło
35
Właściwie nie wierzę, że GC w ogóle wywołuje Object.Finalize, jeśli nie jest nadpisane. Obiekt jest zdeterminowany, aby skutecznie nie mieć finalizatora, a finalizacja jest pomijana - co czyni go bardziej wydajnym, ponieważ obiekt nie musi znajdować się w kolejkach finalizacji / zwolnienia.
Jon Skeet,
7
Zgodnie z MSDN: msdn.microsoft.com/en-us/library/… nie można w rzeczywistości „przesłonić” metody Object.Finalize w języku C #, kompilator generuje błąd: nie zastępuj obiektu.Finalize. Zamiast tego zapewnij destruktor. ; tzn. musisz zaimplementować destruktor, który skutecznie działa jako Finalizer. [dodano tutaj tylko dla kompletności, ponieważ jest to zaakceptowana odpowiedź i najprawdopodobniej zostanie przeczytana]
Sudhanshu Mishra,
1
GC nic nie robi z obiektem, który nie zastępuje Finalizera. Nie jest umieszczany w kolejce finalizacji - i nie jest wywoływany żaden finalizator.
Dave Black,
1
@dotnetguy - chociaż oryginalna specyfikacja C # wspomina o „destruktorze”, w rzeczywistości nazywa się to Finalizatorem - a jego mechanika jest zupełnie inna niż to, jak działa prawdziwy „destruktor” dla języków niezarządzanych.
Dave Black,
67

Chcę podkreślić punkt widzenia Briana w jego komentarzu, ponieważ jest on ważny.

Finalizatory nie są deterministycznymi destruktorami, jak w C ++. Jak zauważyli inni, nie ma gwarancji, kiedy zostanie wywołana, a jeśli masz wystarczająco dużo pamięci, czy kiedykolwiek zostanie wywołana.

Ale złą rzeczą w finalizatorach jest to, że, jak powiedział Brian, powoduje to, że twój obiekt przetrwa czyszczenie pamięci. To może być złe. Czemu?

Jak pewnie wiesz lub nie, GC jest podzielony na pokolenia - Gen 0, 1 i 2 oraz stertę dużych obiektów. Podział to luźny termin - otrzymujesz jeden blok pamięci, ale są wskazówki, gdzie zaczynają się i kończą obiekty Gen 0.

Proces myślowy jest taki, że prawdopodobnie użyjesz wielu obiektów, które będą krótkotrwałe. Więc to powinno być łatwe i szybkie, aby GC mógł dotrzeć do obiektów Gen 0. Kiedy więc występuje presja pamięci, pierwszą rzeczą, którą robi, jest kolekcja Gen 0.

Teraz, jeśli to nie rozwiązuje wystarczającego nacisku, to wraca i wykonuje zamiatanie Gen 1 (ponawianie Gen 0), a następnie, jeśli nadal nie wystarcza, wykonuje zamiatanie Gen 2 (ponawianie Gen 1 i Gen 0). Dlatego czyszczenie obiektów o długiej żywotności może zająć trochę czasu i być dość kosztowne (ponieważ wątki mogą zostać zawieszone podczas operacji).

Oznacza to, że jeśli zrobisz coś takiego:

~MyClass() { }

Twój obiekt, bez względu na wszystko, będzie żył do generacji 2. Dzieje się tak, ponieważ GC nie ma możliwości wywołania finalizatora podczas czyszczenia pamięci. Tak więc obiekty, które mają zostać sfinalizowane, są przenoszone do specjalnej kolejki, aby zostać wyczyszczone przez inny wątek (wątek finalizujący - który, jeśli zabijesz, powoduje różnego rodzaju złe rzeczy). Oznacza to, że obiekty pozostają dłużej i potencjalnie wymuszają więcej zbierania śmieci.

Tak więc wszystko to ma na celu doprowadzenie do punktu, w którym chcesz użyć IDisposable do czyszczenia zasobów, gdy tylko jest to możliwe, i poważnie spróbować znaleźć sposób na obejście finalizatora. Leży to w najlepszym interesie Twojej aplikacji.

Cory Foy
źródło
8
Zgadzam się, że chcesz używać IDisposable, gdy tylko jest to możliwe, ale powinieneś mieć również finalizator, który wywołuje metodę usuwania. Możesz wywołać GC.SuppressFinalize () w IDispose.Dispose po wywołaniu metody dispose, aby upewnić się, że obiekt nie zostanie umieszczony w kolejce finalizatora.
jColeson
2
Pokolenia są ponumerowane 0-2, a nie 1-3, ale Twój post jest poza tym dobry. Dodałbym jednak do tego, że wszelkie obiekty, do których odwołuje się twój obiekt, lub wszelkie obiekty, do których się odwołują, itp. Będą również chronione przed zbieraniem śmieci (choć nie przed finalizacją) przez następne pokolenie. Dlatego obiekty z finalizatorami nie powinny zawierać odniesień do niczego, co nie jest potrzebne do finalizacji.
supercat
3
Jeśli chodzi o „Twój obiekt, bez względu na wszystko, będzie żył do generacji 2.” To BARDZO podstawowa informacja! Zaoszczędziło to dużo czasu podczas debugowania systemu, w którym było wiele krótkotrwałych obiektów Gen2 „przygotowanych” do finalizacji, ale nigdy nie sfinalizowanych spowodowało OutOfMemoryException z powodu dużego wykorzystania sterty. Po usunięciu (nawet pustego) finalizatora i przeniesieniu (obejściu) kodu w inne miejsce, problem zniknął i GC był w stanie obsłużyć obciążenie.
temperówka
@CoryFoy "Twój obiekt, bez względu na wszystko, będzie żył do generacji 2" Czy istnieje jakaś dokumentacja na ten temat?
Ashish Negi
33

Jest tu już dużo dobrej dyskusji i trochę się spóźniłem na imprezę, ale sam chciałem dodać kilka punktów.

  • Moduł wyrzucania elementów bezużytecznych nigdy nie wykona bezpośrednio metody Dispose.
  • GC wykona finalizatory, kiedy przyjdzie na to ochota.
  • Jednym z typowych wzorców używanych dla obiektów, które mają finalizator, jest wywołanie metody, która jest przez konwencję zdefiniowana jako Dispose (bool disposing) przekazująca false, aby wskazać, że wywołanie zostało wykonane z powodu finalizacji, a nie jawnego wywołania Dispose.
  • Dzieje się tak, ponieważ nie jest bezpieczne przyjmowanie jakichkolwiek założeń dotyczących innych zarządzanych obiektów podczas finalizowania obiektu (mogły one już zostać sfinalizowane).

class SomeObject : IDisposable {
 IntPtr _SomeNativeHandle;
 FileStream _SomeFileStream;

 // Something useful here

 ~ SomeObject() {
  Dispose(false);
 }

 public void Dispose() {
  Dispose(true);
 }

 protected virtual void Dispose(bool disposing) {
  if(disposing) {
   GC.SuppressFinalize(this);
   //Because the object was explicitly disposed, there will be no need to 
   //run the finalizer.  Suppressing it reduces pressure on the GC

   //The managed reference to an IDisposable is disposed only if the 
   _SomeFileStream.Dispose();
  }

  //Regardless, clean up the native handle ourselves.  Because it is simple a member
  // of the current instance, the GC can't have done anything to it, 
  // and this is the onlyplace to safely clean up

  if(IntPtr.Zero != _SomeNativeHandle) {
   NativeMethods.CloseHandle(_SomeNativeHandle);
   _SomeNativeHandle = IntPtr.Zero;
  }
 }
}

To prosta wersja, ale istnieje wiele niuansów, które mogą potknąć się o ten wzór.

  • Kontrakt dla IDisposable.Dispose wskazuje, że wielokrotne wywoływanie musi być bezpieczne (wywołanie metody Dispose na obiekcie, który został już usunięty, nie powinno nic robić)
  • Prawidłowe zarządzanie hierarchią dziedziczenia obiektów jednorazowego użytku może być bardzo skomplikowane, zwłaszcza jeśli różne warstwy wprowadzają nowe zasoby jednorazowe i niezarządzane. W powyższym wzorcu Dispose (bool) jest wirtualne, aby umożliwić jego zastąpienie, aby można było nim zarządzać, ale uważam, że jest podatny na błędy.

Moim zdaniem znacznie lepiej jest całkowicie unikać jakichkolwiek typów, które bezpośrednio zawierają zarówno jednorazowe odwołania, jak i zasoby natywne, które mogą wymagać finalizacji. SafeHandles zapewnia bardzo czysty sposób na zrobienie tego poprzez hermetyzację zasobów natywnych do jednorazowych, które wewnętrznie zapewniają własną finalizację (wraz z szeregiem innych korzyści, takich jak usunięcie okna podczas P / Invoke, gdzie natywny uchwyt może zostać utracony z powodu asynchronicznego wyjątku) .

Po prostu zdefiniowanie SafeHandle sprawia, że ​​ta trywialna:


private class SomeSafeHandle
 : SafeHandleZeroOrMinusOneIsInvalid {
 public SomeSafeHandle()
  : base(true)
  { }

 protected override bool ReleaseHandle()
 { return NativeMethods.CloseHandle(handle); }
}

Pozwala uprościć typ zawierający:


class SomeObject : IDisposable {
 SomeSafeHandle _SomeSafeHandle;
 FileStream _SomeFileStream;
 // Something useful here
 public virtual void Dispose() {
  _SomeSafeHandle.Dispose();
  _SomeFileStream.Dispose();
 }
}
Andrzej
źródło
1
Skąd pochodzi klasa SafeHandleZeroOrMinusOneIsInvalid? Czy jest to wbudowany typ .net?
Orion Edwards,
+1 dla // Moim zdaniem znacznie lepiej jest całkowicie unikać jakichkolwiek typów, które bezpośrednio zawierają zarówno jednorazowe referencje, jak i zasoby natywne, które mogą wymagać finalizacji .// Jedynymi nieopieczętowanymi klasami, które powinny mieć finalizatory, są te, których cel koncentruje się na finalizacja.
supercat
1
@OrionEdwards tak patrz msdn.microsoft.com/en-us/library/…
Martin Capodici
1
Odnośnie wezwania do GC.SuppressFinalizew tym przykładzie. W tym kontekście SuppressFinalize powinien być wywoływany tylko wtedy, gdy zostanie Dispose(true)wykonany pomyślnie. Jeśli Dispose(true)zakończy się niepowodzeniem w pewnym momencie po wstrzymaniu finalizacji, ale zanim wszystkie zasoby (szczególnie niezarządzane) zostaną wyczyszczone, nadal chcesz, aby finalizacja nastąpiła, aby wykonać jak najwięcej czyszczenia. Lepiej przenieść GC.SuppressFinalizewywołanie do Dispose()metody po wywołaniu funkcji Dispose(true). Zobacz wytyczne dotyczące projektowania ram i ten post .
BitMask777
6

Nie sądzę. Masz kontrolę nad wywołaniem metody Dispose, co oznacza, że ​​teoretycznie możesz napisać kod usuwania, który przyjmuje założenia dotyczące (na przykład) istnienia innych obiektów. Nie masz kontroli nad tym, kiedy wywoływany jest finalizator, więc byłoby nieefektywne, gdyby finalizator automatycznie wywoływał Dispose w Twoim imieniu.


EDYCJA: Odszedłem i przetestowałem, aby się upewnić:

class Program
{
    static void Main(string[] args)
    {
        Fred f = new Fred();
        f = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine("Fred's gone, and he's not coming back...");
        Console.ReadLine();
    }
}

class Fred : IDisposable
{
    ~Fred()
    {
        Console.WriteLine("Being finalized");
    }

    void IDisposable.Dispose()
    {
        Console.WriteLine("Being Disposed");
    }
}
Matt Bishop
źródło
Przyjmowanie założeń dotyczących obiektów, które są dostępne podczas utylizacji, może być niebezpieczne i trudne, zwłaszcza podczas finalizacji.
Scott Dorman
3

Nie w przypadku, który opisujesz, ale GC zadzwoni do Finalizatora , jeśli go masz.

JEDNAK. Przy następnym wyrzucaniu elementów bezużytecznych, zamiast zbierania, obiekt przejdzie do kolejki finalizacji, wszystko zostanie zebrane, a następnie zostanie wywołany finalizator. Następna kolekcja zostanie uwolniona.

W zależności od obciążenia pamięci aplikacji przez pewien czas możesz nie mieć gc do generowania tego obiektu. Tak więc w przypadku, powiedzmy, strumienia plików lub połączenia db, być może trzeba będzie chwilę poczekać, aż niezarządzany zasób zostanie przez chwilę zwolniony w wywołaniu finalizatora, powodując pewne problemy.

Brian Leahy
źródło
1

Nie, to nie jest wywołane.

Ale to sprawia, że ​​łatwo nie zapomnieć o pozbyciu się przedmiotów. Po prostu użyjusing słowa kluczowego.

Zrobiłem w tym celu następujący test:

class Program
{
    static void Main(string[] args)
    {
        Foo foo = new Foo();
        foo = null;
        Console.WriteLine("foo is null");
        GC.Collect();
        Console.WriteLine("GC Called");
        Console.ReadLine();
    }
}

class Foo : IDisposable
{
    public void Dispose()
    {

        Console.WriteLine("Disposed!");
    }
penyaskito
źródło
1
To był przykład tego, jak jeśli NIE użyjesz słowa kluczowego <code> using </code>, nie zostanie ono nazwane ... a ten fragment ma 9 lat, wszystkiego najlepszego!
penyaskito
1

GC nie wywoła dispose. To może wywołać swoją finalizatora, ale nawet to nie jest gwarantowana w każdych okolicznościach.

W tym artykule omówiono najlepsze sposoby radzenia sobie z tym problemem.

Rob Walker
źródło
0

Dokumentacja IDisposable zawiera dość jasne i szczegółowe wyjaśnienie zachowania, a także przykładowy kod. GC NIE wywoła Dispose()metody w interfejsie, ale wywoła finalizator dla twojego obiektu.

Joseph Daigle
źródło
0

Wzorzec IDisposable został utworzony głównie w celu wywołania przez dewelopera, jeśli masz obiekt, który implementuje IDispose, deweloper powinien zaimplementować usingsłowo kluczowe wokół kontekstu obiektu lub bezpośrednio wywołać metodę Dispose.

Bezpiecznym niepowodzeniem dla wzorca jest zaimplementowanie finalizatora wywołującego metodę Dispose (). Jeśli tego nie zrobisz, możesz spowodować wycieki pamięci, np .: Jeśli utworzysz opakowanie COM i nigdy nie wywołasz System.Runtime.Interop.Marshall.ReleaseComObject (comObject) (który zostałby umieszczony w metodzie Dispose).

W clr nie ma żadnej magii do automatycznego wywoływania metod Dispose, innych niż śledzenie obiektów, które zawierają finalizatory i przechowywanie ich w tabeli Finalizer przez GC i wywoływanie ich, gdy GC uruchomi niektóre heurystyki czyszczenia.

Erick Sgarbi
źródło