Ruft der Garbage Collector IDisposable.Dispose für mich auf?

134

Das .NET IDisposable-Muster impliziert , dass Ihr Finalizer Dispose explizit aufrufen muss, wenn Sie einen Finalizer schreiben und IDisposable implementieren. Dies ist logisch und das habe ich immer in den seltenen Situationen getan, in denen ein Finalizer erforderlich ist.

Was passiert jedoch, wenn ich dies einfach mache:

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

und implementieren Sie keinen Finalizer oder so. Wird das Framework die Dispose-Methode für mich aufrufen?

Ja, mir ist klar, dass dies dumm klingt, und jede Logik impliziert, dass dies nicht der Fall ist, aber ich hatte immer zwei Dinge im Hinterkopf, die mich unsicher gemacht haben.

  1. Jemand hat mir vor ein paar Jahren einmal gesagt, dass dies tatsächlich der Fall sein würde, und diese Person hatte eine sehr solide Erfolgsbilanz darin, "ihre Sachen zu kennen".

  2. Der Compiler / das Framework führt andere "magische" Dinge aus, je nachdem, welche Schnittstellen Sie implementieren (z. B. foreach, Erweiterungsmethoden, Serialisierung basierend auf Attributen usw.). Daher ist es sinnvoll, dass dies auch "magisch" ist.

Obwohl ich viel darüber gelesen habe und viele Dinge impliziert wurden, konnte ich auf diese Frage nie eine endgültige Ja- oder Nein-Antwort finden.

Orion Edwards
quelle

Antworten:

121

Der .Net Garbage Collector ruft die Object.Finalize-Methode eines Objekts für die Garbage Collection auf. Mit dem Standard tut dies nichts und muss außer Kraft gesetzt , wenn Sie zusätzliche Ressourcen frei wollen.

Dispose wird NICHT automatisch aufgerufen und muss explizit aufgerufen werden, wenn Ressourcen freigegeben werden sollen, z. B. innerhalb eines Blocks "using" oder "try finally"

Weitere Informationen finden Sie unter http://msdn.microsoft.com/en-us/library/system.object.finalize.aspx

Xian
quelle
35
Eigentlich glaube ich nicht, dass der GC Object.Finalize aufruft, wenn es nicht überschrieben wird. Es wird festgestellt, dass das Objekt effektiv keinen Finalizer hat, und die Finalisierung wird unterdrückt - was es effizienter macht, da sich das Objekt nicht in den Finalisierungs- / Freachable-Warteschlangen befinden muss.
Jon Skeet
7
Gemäß MSDN: msdn.microsoft.com/en-us/library/… können Sie die Object.Finalize-Methode in C # nicht "überschreiben". Der Compiler generiert einen Fehler: Objekt.Finalize nicht überschreiben. Geben Sie stattdessen einen Destruktor an. ;; Das heißt, Sie müssen einen Destruktor implementieren, der effektiv als Finalizer fungiert. [der Vollständigkeit
halber
1
Der GC führt nichts mit einem Objekt aus, das einen Finalizer nicht überschreibt. Es wird nicht in die Finalisierungswarteschlange gestellt - und es wird kein Finalizer aufgerufen.
Dave Black
1
@dotnetguy - obwohl die ursprüngliche C # -Spezifikation einen "Destruktor" erwähnt, wird sie tatsächlich als Finalizer bezeichnet - und ihre Mechanik unterscheidet sich grundlegend von der Funktionsweise eines echten "Destruktors" für nicht verwaltete Sprachen.
Dave Black
67

Ich möchte Brians Punkt in seinem Kommentar hervorheben, weil es wichtig ist.

Finalizer sind keine deterministischen Destruktoren wie in C ++. Wie andere bereits betont haben, gibt es keine Garantie dafür, wann es aufgerufen wird, und wenn Sie über genügend Speicher verfügen, wird es jemals aufgerufen.

Aber das Schlechte an Finalisierern ist, dass Ihr Objekt, wie Brian sagte, eine Speicherbereinigung überlebt. Das kann schlecht sein. Warum?

Wie Sie vielleicht wissen oder nicht wissen, ist der GC in Generationen unterteilt - Gen 0, 1 und 2 sowie den Heap für große Objekte. Split ist ein loser Begriff - Sie erhalten einen Speicherblock, aber es gibt Hinweise darauf, wo die Gen 0-Objekte beginnen und enden.

Der Denkprozess ist, dass Sie wahrscheinlich viele Objekte verwenden werden, die nur von kurzer Dauer sind. Diese sollten also für den GC einfach und schnell sein, um zu - Gen 0-Objekten zu gelangen. Wenn also Speicherdruck herrscht, ist das erste, was es tut, eine Gen 0-Sammlung.

Wenn das nicht genug Druck auflöst, geht es zurück und führt einen Gen 1-Sweep durch (Wiederherstellen von Gen 0). Wenn dies immer noch nicht ausreicht, führt es einen Gen 2-Sweep durch (Wiederherstellen von Gen 1 und Gen 0). Das Reinigen langlebiger Objekte kann daher eine Weile dauern und ziemlich teuer sein (da Ihre Fäden während des Vorgangs möglicherweise hängen bleiben).

Dies bedeutet, wenn Sie so etwas tun:

~MyClass() { }

Ihr Objekt wird, egal was passiert, der 2. Generation entsprechen. Dies liegt daran, dass der GC den Finalizer während der Speicherbereinigung nicht aufrufen kann. Objekte, die finalisiert werden müssen, werden in eine spezielle Warteschlange verschoben, um von einem anderen Thread (dem Finalizer-Thread - der beim Töten alle möglichen schlechten Dinge verursacht) gelöscht zu werden. Dies bedeutet, dass Ihre Objekte länger herumhängen und möglicherweise mehr Speicherbereinigungen erzwingen.

All dies dient nur dazu, den Punkt nach Hause zu bringen, an dem Sie IDisposable verwenden möchten, um Ressourcen zu bereinigen, wann immer dies möglich ist, und ernsthaft zu versuchen, mit dem Finalizer Wege zu finden. Es ist im besten Interesse Ihrer Anwendung.

Cory Foy
quelle
8
Ich bin damit einverstanden, dass Sie IDisposable verwenden möchten, wann immer dies möglich ist, aber Sie sollten auch einen Finalizer haben, der eine Dispose-Methode aufruft. Sie können GC.SuppressFinalize () in IDispose.Dispose aufrufen, nachdem Sie Ihre dispose-Methode aufgerufen haben, um sicherzustellen, dass Ihr Objekt nicht in die Finalizer-Warteschlange gestellt wird.
jColeson
2
Generationen sind mit 0-2 nummeriert, nicht mit 1-3, aber Ihr Beitrag ist ansonsten gut. Ich möchte jedoch hinzufügen, dass alle Objekte, auf die von Ihrem Objekt verwiesen wird, oder alle Objekte, auf die von diesen usw. verwiesen wird, auch für eine andere Generation gegen Speicherbereinigung (jedoch nicht gegen Finalisierung) geschützt werden. Daher sollten Objekte mit Finalisierern keine Verweise auf etwas enthalten, das für die Finalisierung nicht benötigt wird.
Supercat
3
In Bezug auf "Ihr Objekt, egal was passiert, wird Generation 2 leben." Dies ist eine sehr grundlegende Information! Es hat viel Zeit beim Debuggen eines Systems gespart, bei dem viele kurzlebige Gen2-Objekte für die Finalisierung "vorbereitet", aber nie finalisiert wurden, was aufgrund der starken Heap-Nutzung zu einer OutOfMemoryException führte. Durch Entfernen des (sogar leeren) Finalizers und Verschieben (Umgehen) des Codes an eine andere Stelle verschwand das Problem und der GC konnte die Last bewältigen.
Anspitzer
@CoryFoy "Ihr Objekt, egal was passiert, wird Generation 2 entsprechen" Gibt es eine Dokumentation dafür?
Ashish Negi
33

Hier gibt es bereits viele gute Diskussionen, und ich bin etwas spät zur Party, aber ich wollte selbst ein paar Punkte hinzufügen.

  • Der Garbage Collector führt niemals direkt eine Dispose-Methode für Sie aus.
  • Der GC führt Finalizer aus, wenn er Lust dazu hat.
  • Ein häufiges Muster, das für Objekte mit einem Finalizer verwendet wird, besteht darin, eine Methode aufzurufen, die gemäß der Konvention als Dispose (bool disposing) definiert ist und false übergibt, um anzuzeigen, dass der Aufruf aufgrund der Finalisierung und nicht aufgrund eines expliziten Dispose-Aufrufs erfolgt ist.
  • Dies liegt daran, dass es nicht sicher ist, beim Finalisieren eines Objekts Annahmen über andere verwaltete Objekte zu treffen (diese wurden möglicherweise bereits finalisiert).

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;
  }
 }
}

Das ist die einfache Version, aber es gibt viele Nuancen, die Sie über dieses Muster stolpern lassen können.

  • Der Vertrag für IDisposable.Dispose gibt an, dass es sicher sein muss, mehrmals anzurufen (das Aufrufen von Dispose für ein Objekt, das bereits entsorgt wurde, sollte nichts bewirken).
  • Es kann sehr kompliziert werden, eine Vererbungshierarchie von verfügbaren Objekten ordnungsgemäß zu verwalten, insbesondere wenn verschiedene Ebenen neue verfügbare und nicht verwaltete Ressourcen einführen. Im obigen Muster ist Dispose (bool) virtuell, damit es überschrieben werden kann, damit es verwaltet werden kann, aber ich finde es fehleranfällig.

Meiner Meinung nach ist es viel besser, Typen zu vermeiden, die direkt sowohl verfügbare Referenzen als auch native Ressourcen enthalten, die möglicherweise finalisiert werden müssen. SafeHandles bieten eine sehr saubere Möglichkeit, dies zu tun, indem native Ressourcen in Einwegressourcen gekapselt werden, die intern ihre eigene Finalisierung bereitstellen (zusammen mit einer Reihe anderer Vorteile wie dem Entfernen des Fensters während P / Invoke, bei dem ein natives Handle aufgrund einer asynchronen Ausnahme verloren gehen könnte). .

Das einfache Definieren eines SafeHandle macht diese Trivialität:


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

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

Ermöglicht die Vereinfachung des enthaltenen Typs auf:


class SomeObject : IDisposable {
 SomeSafeHandle _SomeSafeHandle;
 FileStream _SomeFileStream;
 // Something useful here
 public virtual void Dispose() {
  _SomeSafeHandle.Dispose();
  _SomeFileStream.Dispose();
 }
}
Andrew
quelle
1
Woher kommt die SafeHandleZeroOrMinusOneIsInvalid-Klasse? Ist es ein eingebauter .net-Typ?
Orion Edwards
+1 für // Meiner Meinung nach ist es viel besser, Typen zu vermeiden, die direkt sowohl verfügbare Verweise als auch native Ressourcen enthalten, die möglicherweise finalisiert werden müssen. / / Die einzigen nicht versiegelten Klassen, die jemals Finalisierer haben sollten, sind diejenigen, deren Zweck sich darauf konzentriert Finalisierung.
Supercat
1
@OrionEdwards ja siehe msdn.microsoft.com/en-us/library/…
Martin Capodici
1
In Bezug auf den Aufruf GC.SuppressFinalizein diesem Beispiel. In diesem Zusammenhang sollte SuppressFinalize nur aufgerufen werden, wenn es Dispose(true)erfolgreich ausgeführt wird. Wenn Dispose(true)irgendwann nach der Unterdrückung der Finalisierung ein Fehler auftritt, aber bevor alle Ressourcen (insbesondere nicht verwaltete) bereinigt werden, möchten Sie dennoch die Finalisierung durchführen, um so viele Bereinigungen wie möglich durchzuführen. Es ist besser, den GC.SuppressFinalizeAufruf nach dem Aufruf von in die Dispose()Methode zu verschieben Dispose(true). Siehe Framework Design Guidelines und diesen Beitrag .
BitMask777
6

Das glaube ich nicht. Sie haben die Kontrolle darüber, wann Dispose aufgerufen wird. Dies bedeutet, dass Sie theoretisch Entsorgungscode schreiben können, der Annahmen über (zum Beispiel) die Existenz anderer Objekte macht. Sie haben keine Kontrolle darüber, wann der Finalizer aufgerufen wird. Daher ist es zweifelhaft, wenn der Finalizer Dispose automatisch in Ihrem Namen aufruft.


EDIT: Ich ging weg und testete, nur um sicherzugehen:

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
quelle
Es kann gefährlich und schwierig sein, Annahmen über die Objekte zu treffen, die Ihnen während der Entsorgung zur Verfügung stehen, insbesondere während der Fertigstellung.
Scott Dorman
3

Nicht in dem von Ihnen beschriebenen Fall, aber der GC ruft den Finalizer für Sie an, falls Sie einen haben.

JEDOCH. Bei der nächsten Garbage Collection wird das Objekt nicht gesammelt, sondern in die Finalisierungswarteschlange gestellt, alles wird gesammelt und der Finalizer aufgerufen. Die nächste Sammlung danach wird freigegeben.

Abhängig vom Speicherdruck Ihrer App haben Sie möglicherweise für eine Weile keinen GC für diese Objekterzeugung. Im Fall eines Dateistreams oder einer Datenbankverbindung müssen Sie möglicherweise eine Weile warten, bis die nicht verwaltete Ressource im Finalizer-Aufruf für eine Weile freigegeben wird, was zu Problemen führt.

Brian Leahy
quelle
1

Nein, es heißt nicht.

Dies macht es jedoch leicht, nicht zu vergessen, Ihre Gegenstände zu entsorgen. Verwenden Sie einfach dieusing Schlüsselwort.

Ich habe dazu folgenden Test durchgeführt:

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
quelle
1
Dies war ein Beispiel dafür, wie wenn Sie das Schlüsselwort <code> using </ code> NICHT verwenden, es nicht aufgerufen wird ... und dieses Snippet hat 9 Jahre, alles Gute zum Geburtstag!
Penyaskito
1

Der GC ruft nicht dispose auf. Es kann Ihre Finalizerthread nennen, aber auch dies ist nicht unter allen Umständen garantiert.

In diesem Artikel erfahren Sie, wie Sie am besten damit umgehen können.

Rob Walker
quelle
0

Die Dokumentation zu IDisposable enthält eine ziemlich klare und detaillierte Erläuterung des Verhaltens sowie Beispielcode. Der GC ruft die Dispose()Methode NICHT auf der Schnittstelle auf, sondern den Finalizer für Ihr Objekt.

Joseph Daigle
quelle
0

Das IDisposable-Muster wurde hauptsächlich erstellt, um vom Entwickler aufgerufen zu werden. Wenn Sie ein Objekt haben, das IDispose implementiert, sollte der Entwickler entweder das implementieren using Schlüsselwort um den Kontext des Objekts oder die Dispose-Methode direkt aufrufen.

Die Ausfallsicherheit für das Muster besteht darin, den Finalizer zu implementieren, der die Dispose () -Methode aufruft. Wenn Sie dies nicht tun, können Sie einige Speicherlecks verursachen, z. B.: Wenn Sie einen COM-Wrapper erstellen und niemals System.Runtime.Interop.Marshall.ReleaseComObject (comObject) aufrufen (das in der Dispose-Methode platziert wird).

Es gibt keine Magie in der CLR, Dispose-Methoden automatisch aufzurufen, außer Objekte zu verfolgen, die Finalizer enthalten, und diese in der Finalizer-Tabelle vom GC zu speichern und sie aufzurufen, wenn einige Bereinigungsheuristiken vom GC aktiviert werden.

Erick Sgarbi
quelle