Garbage Collection und Dispose Pattern (eine Zusammenfassung von Video-Tuts)

    • VB.NET

      Garbage Collection und Dispose Pattern (eine Zusammenfassung von Video-Tuts)

      Hallo,

      @ISliceUrPanties hat diese beiden Videos als Hilfestellung gefunden, wenn es um Verständnis bezüglich GarbageCollection und Dispose-Pattern geht. GarbageCollection, Dispose-Pattern

      Diese sind englisch und c#. Deswegen hier eine Zusammenfassung in deutsch und vb.net. Ich bin da auch kein Fachmann, daher kann ich nur darbringen, was die Videos vermitteln, nicht mehr.

      GarbageCollection:

      Es wird nicht darauf eingegangen, was der Stack oder was der Heap sind. Sie werden zur Veranschaulichung benutzt wann Daten vorhanden sind und wann nicht.

      Benutzter Code:
      personclass

      VB.NET-Quellcode

      1. Public Class Person
      2. Public Property Name As String
      3. Public Property KindEins As Person
      4. Public Property KindZwei As Person
      5. Protected Overrides Sub Finalize()
      6. Console.Writeline($" Collecting {Name}.")
      7. End Sub
      8. End Class


      Eine Person hat einen Namen und beispielsweise zwei Kinder, (sinnvollererweise eine List an Kindern, aber hier gehts nicht um den Sinn der Klasse)
      Außerdem überschreibt die Person, ihre Finalize-Methode, welche jedes Object besitzt. Die Überschreibung ist hier ebenfalls keine sinnvolle Überschreibung, sie soll explizit nur den Aufruf der Methode sichtbar machen.

      Programm

      VB.NET-Quellcode

      1. Class Programm
      2. Private Shared Sub ErzeugeLeben(elternteil As Person)
      3. Dim fred As Person = New Person With {
      4. .Name = "Fred",
      5. .KindEins = New Person With {.Name = "Bamm-Bamm"}
      6. }
      7. parent.KindZwei = fred.KindEins
      8. End Sub
      9. Private Shared Sub Run()
      10. Dim wilma As Person = New Person With {
      11. .Name = "Wilma",
      12. .KindEins = New Person With {.Name = "Pebbles"}
      13. }
      14. ErzeugeLeben(wilma)
      15. Console.Writeline("Leaving 'ErzeugeLeben'...")
      16. GC.Collect()
      17. GC.WaitForPendingFinalizers()
      18. End Sub
      19. Private Shared Sub Main()
      20. Run()
      21. Console.Writeline(vbLf & "Leaving 'Run'...")
      22. GC.Collect()
      23. GC.WaitForPendingFinalizers()
      24. End Sub
      25. End Class


      In Run() wird wilma, mit bereits einem Kind erzeugt. ErzeugeLeben() fügt wilma ein weiteres Kind hinzu, dazu wird eine weitere Person benötigt, welche kurzfristig in dieser Methode erzeugt (fred) wird. freds erstes Kind wird zu wilmas zweitem Kind.

      GC.Collect wird normalerweise nicht in angewandtem Code benötigt. Denn diese Methode wird vom System automatisch ausgelöst, wenn sie benötigt wird. Hier soll sie die Collection zu dem Zeitpunkt erzwingen wie er für uns nützlich ist, um zu sehen was passiert. Ein solcher Zeitpunkt ist nach Ende der Methode ErzeugeLeben und ein weiterer nach der Methode Run. GC.Collect aktiviert automatisch die Finalizer Methode von Objekten. Aber von welchen Objekten?

      Am Ende der ErzeugeLeben Methode befinden sich im Stack die Variablen fred, parent und wilma, wobei fred auf das Objekt "Fred" und parent und wilma auf das Objekt "Wilma" im Heap verweisen.
      Das Objekt "Fred" verweist über seine KindEins-Property außerdem auf das Objekt "Bamm-Bamm" und Objekt "Wilma" verweist in gleicher Manier auf die Objekte "Pebbles" und "Bamm-Bamm" im Heap.

      Wird die Methode verlassen, verschwinden fred und parent aus dem Stack.

      Bei der GarbageCollection geht der GC durch alle lokalen Variablen und shared Variablen im Stack und markiert als erreichbar
      - die Daten, auf welche diese Variablen verweisen,
      - und die Daten, auf welche die markierten Daten verweisen.

      In unserer ersten GarbageCollection befindet sich nur wilma im Stack, markiert wird daher "Wilma" und daher "Pebbles" und "Bamm-Bamm". Anschließend werden die unmarkierten Daten entfernt, das heißt deren Finalizer wird aufgerufen
      Entsprechend erhalten wir an dieser Stelle den Output "Collecting Fred."

      Verlassen wir nun die Methode Run verschwindet auch wilma aus dem Stack. Wir stoßen erneut ein Collection an und nun bekommen die Objekte "Wilma", "Pebbles" und "Bamm-Bamm" keine Markierung mehr, sie werden entfernt.
      Was in unserem Beispielcode nur die Ausgabe "Collecting Bamm-Bamm.", "Collecting Pebbles.", "Collecting Wilma" bedeutet. Hier sieht man auch, dass erst der Inhalt von "Wilma" vernichtet wird und dann "Wilma" selbst.
      ---------------------------------------------------------------------------------------------------

      Effizienz der GarbageCollection:

      Das echte System bedient sich eines Prinzips, das die Effizienz verbessert.
      Eigentlich gibt es 4 Heaps. Generation 0-, 1- und 2- und den Large-Object-Heap. Der Large-Object-Heap beinhaltet Objekte > 85KB
      Der GC läuft wie gesagt automatisch und schaut dabei unterschiedlich häufig in die unterschiedlichen Heaps.

      Am häufigsten wird der Generation 0-Heap überprüft. Alle Objekte die erreichbar sind, werden in den Generation 1-Heap verfrachtet. Anschließend wird der Generation 0-Heap gar nicht gelöscht, sondern der Heap-Zeiger wird wieder auf den Anfang gesetzt, was neue Daten alles überschreiben lässt, was da ist oder auch nicht da ist.

      Nichtmehr ganz so häufig wird der Generation 1-Heap überprüft, denn da sind ja nur Objekte drin, die schon mindestens einmal gezeigt haben, dass sie eine gewisse Dauer überstehen. Hier bleibt der Ablauf gleich.

      Der Generation 2-Heap wird nur selten geprüft. Der Ablauf entspricht hier wie weiter oben geschildert; d.h. hier müssen die Objekte wirklich gelöscht werden, was daher etwas länger dauert. Der Large-Object-Heap verhält sich wie der Generation 2-Heap, basierend auf der Annahme "große Objekte sind langlebige Objekte". Deswegen kommt großen Objekten gegebenenfalls eine besondere Bedeutung zu, wenn sie mal nicht besonders langlebig sein sollten, denn sie werden trotzdem nur selten vom GC angeguckt.


      Sowohl das Nicht-Löschen, als auch die statistisch plausible Verteilung der Überprüfungsfrequenz auf die verschiedenen Heaps sorgen für eine gute Effizienz.
      ---------------------------------------------------------------------------------------------------

      Disposen:

      Wir haben gesehen, dass der GC Objekte Finalized. Normalerweise wird ein Finalizer nur selten angefasst.

      Am meisten hat er Auswirkungen dann, wenn man das Disposen vergisst. Denn Finalizen ist an einigen Stellen nicht effizient, weil es nicht in unserer Hand liegt, denn der GC soll unabhängig vom Programmierer arbeiten.
      Also in erster Linie bedeutet die Implementation von Dispose nur einen Effizienzgewinn. (Überwiegend was die Memory betrifft und Blockaden bestimmter Zugriffe, denn wie wir nun wissen, wann der Finalizer kommt, bestimmen normalerweise nicht wir)
      Mit Dispose bestimmen wir selbst.

      Benutzter Code:
      Fall1

      VB.NET-Quellcode

      1. Imports System.IO
      2. Class Programm
      3. Private Shared Sub Run()
      4. Using kvk = New KomplettVerwalteteKlasse()
      5. kvk.StartWriting()
      6. End Using
      7. End Sub
      8. Private Shared Sub Main()
      9. Run()
      10. GC.Collect()
      11. GC.WaitPendingFinalizers()
      12. End Sub
      13. End Class
      14. Public Class KomplettVerwalteteKlasse
      15. Implements IDisposable
      16. Private _writer As StreamWriter
      17. Public Sub StartWriting()
      18. _writer = New StreamWriter("Ausgabe.txt")
      19. End Sub
      20. Public Sub Dispose() Implements IDisposable.Dispose
      21. Console.Writeline("Disposing")
      22. _writer?.Dispose()
      23. End Sub
      24. ' Protected Overrides Sub Finalize()
      25. ' Console.WriteLine("Finalizing")
      26. ' End Sub
      27. End Class


      Ein StreamWriter öffnet eine Datei und blockiert damit den Zugriff. Um den Zugriff wiederzuerlangen, muss der StreamWriter zerstört werden. Dazu implementiert diese Klasse bereits die IDisposable Schnittstelle und ermöglicht durch Aufruf der Dispose-Methode diesen Zeitpunkt zu bestimmen.

      Befindet sich der StreamWriter in einer weiteren Klasse, hier im Beispiel die KomplettVerwalteteKlasse, dann haben wir gesehen, hat diese Klasse einen Verweis auf den StreamWriter und ist somit für seine Lebensspanne verantwortlich.
      Wir müssen also den Zeitpunkt der Zerstörung einer KomplettVerwalteteKlasse-Instanz bestimmen können, damit wir die Zerstörung des StreamWriter bestimmen können. Also implementieren wir hier nun die IDisposable-Schnittstelle, denn die macht ja genau das. Dabei reicht es vollkommen aus das Dispose des StreamWriters zu kaskadieren. Eine Ausgabe in die Konsole hilft uns hier wieder den Vorgang zu betrachten.

      Unser Programm erzeugt nur eine Instanz unserer Klasse und "benutzt" sie.
      Außerdem forcieren wir erneut eine GarbageCollection, damit wir wieder die Auswirkungen sehen, die dieses System ausübt. Damit man hier etwas sieht, muss in unserer KomplettVerwalteteKlasse auch wieder ein Finalizer überschrieben
      werden (Den ruft der GC auf). Er ist auskommentiert, da er an dieser Stelle nicht zur Implementation von IDisposable gehört (eigentlich wollen wir den gar nicht haben), er wird uns hier auch wieder nur eine Ausgabe bescheren.

      Der Using-Block um kvk vernichtet diese Instanz am Ende. Beim Ablauf des Codes erhalten wir entsprechend die Ausgabe "Disposing".
      Lassen wir den Using-Block weg und kommentieren den Finalizer rein, erhalten wir die Botschaft "Finalizing".
      Also es ändert sich nur wer aufräumt. Das sieht man auch, wenn man durch den Code stepped: "Disposing" erscheint am Ende des Using-Blocks, "Finalizing" erscheint beim GC.Collect Kommando.

      Also Using wieder rein. Und siehe da: jetzt passiert natürlich beides, "Disposing" und "Finalizing" erscheinen. Das will man nicht, aus Effizienz-Gründen.
      Daher wird in die Dispose-Methode GC.SuppressFinalize(Me) eingefügt. Nun wird nur noch Disposed.


      Warum ist der Finalizer ineffizient? Wir haben in der Betrachtung der Heaps gesehen, das setzen des Heap-Pointer an den Anfang des Heaps erspart enorm viel Arbeit, aber so ein Objekt wie StreamWriter will gelöscht werden. Denn wir können ja nicht warten bis der von neuem Speicherbedarf überschrieben wird. Finalizer stoppen also das generelle Prinzip der schnellen Heap-Bereinigung. Sie reihen sich in eine Finalizing-Warteschlange auch dann, wenn man den Finalizer als komplett leere Methode überschreibt.

      Ein kleines Spielchen noch: _writer.Dispose im Finalizer führt zu einer Exception, denn während der GarbageCollection, in der wir uns beim Finalizen gerade befinden, wurde _writer bereits vernichtet. Das brauchen wir gleich noch (*)

      Es gilt in Klassen, deren Bestandteile alle verwaltet sind, braucht man keinen Finalizer.
      Das bringt uns zu Klassen, die nicht-verwaltete Bestandteile enthalten.
      ---------------------------------------------------------------------------------------------------

      Dispose-Pattern:

      Wenn wir nicht-verwaltete Ressourcen nutzen, müssen wir sie aufräumen, denn das tun diese genau nicht mehr selbst und dafür bieten sich entsprechende Methodenaufrufe im Finalizer an, denn das ist die Methode die der GC aufruft.
      Dadurch kann das System (wenn nötig) automatisch aufräumen. Wir wollen aber auch weiterhin selbst aufräumen. GC.Collect könnte man hierfür zum Beispiel halbwegs sinnvoll missbrauchen. :S
      Aber verwaltete Ressourcen haben wir höchstwahrscheinlich auch noch, dann geht das schon nicht mehr, weil wir die ja nicht im Finalizer bereinigen können, da dies wie bei (*) gesehen zur Exception führt. Also muss das außerhalb passieren.

      Unsere Dispose-Methode muss also:
      - verwaltete und nicht verwaltete Ressourcen manuell bereinigbar machen
      - nicht verwaltete Ressourcen automatisch bereinigbar machen
      - verwaltete Ressourcen nicht versuchen in der automatischen Bereinigung zu bereinigen.


      Fall2

      VB.NET-Quellcode

      1. Imports System.IO
      2. Imports Excel = Microsoft.Office.Interop.Excel
      3. Imports System.Runtime.InteropServices
      4. Class Programm
      5. Private Shared Sub Run()
      6. Using gk As GemischteKlasse = New GemischteKlasse()
      7. gk.StartWriting()
      8. End Using
      9. End Sub
      10. Private Shared Sub Main()
      11. Run()
      12. End Sub
      13. End Class
      14. Public Class GemischteKlasse
      15. Implements IDisposable
      16. Private _writer As StreamWriter
      17. Private _excel As Excel.Application
      18. Private disposedValue As Boolean
      19. Public Sub StartWriting()
      20. _writer = New StreamWriter("Ausgabe.txt")
      21. _excel = New Excel.Application()
      22. End Sub
      23. Protected Overridable Sub Dispose(ByVal disposing As Boolean)
      24. If Not disposedValue Then
      25. If disposing Then
      26. _writer?.Dispose()
      27. End If
      28. If _excel IsNot Nothing Then
      29. _excel.Quit()
      30. Marshal.ReleaseComObject(_excel)
      31. End If
      32. disposedValue = True
      33. End If
      34. End Sub
      35. Protected Overrides Sub Finalize()
      36. Dispose(disposing:=False)
      37. End Sub
      38. Public Sub Dispose() Implements IDisposable.Dispose
      39. Dispose(disposing:=True)
      40. GC.SuppressFinalize(Me)
      41. End Sub
      42. End Class

      Wenn wir gk.Dispose aufrufen, also am Ende des Using-Blocks, dann rufen wir unsere Dispose Überladung mit Argument True auf. Dadurch werden
      - verwaltete und nicht verwaltete Ressourcen manuell bereinigbar gemacht (Sowohl der Code für Excel als aus für den StreamWriter werden hier bearbeitet)

      Wir überschreiben den Finalizer, dadurch bekommt der GC Zugang zu Methoden, die er sonst nicht hat; also werden die
      - nicht verwalteten Ressourcen automatisch bereinigbar gemacht.
      Da wir einen Finalizer nun verwenden müssen, im Gegensatz zu Fall 1, wollen wir im echten Dispose diesen also unterdrücken (GC.SuppressFinalize(Me))

      Der Finalizer ruft die Dispose Überladung mit Argument False auf und fasst daher nicht den Dispose von unseren verwalteten Ressourcen an.
      - es wird nicht versucht verwaltete Ressourcen in der automatischen Bereinigung zu bereinigen

      Die Bereinigung von Excel hat spezielle Methoden (Quit, ReleaseComObject) und andere nicht-verwaltete Objekte würden hier dementsprechend andere Methoden benötigen.

      Die Abfrage von disposedValue ermöglicht die Mehrfachausführung der Dispose-Methode sie ist damit idempotent. (Aber nur weil alles gelöscht ist, was gelöscht werden soll bei weiteren Durchläufen; Methoden werden nicht generell durch Abschalten von Mehrfachdurchläufen idempotent)




      So das ist was ich nun endlich mitnehmen konnte
      --------------------------------------------------------------------------------------
      Folgende Fehler möchte ich noch anmerken im Beispiel-Code (Ich möchte sie nicht im Code einfach ersetzen, da im Video derselbe "falsche" Code verwendet wird):
      Fall2 benötigt in Zeile 36-39 am Ende noch _excel = Nothing, sonst würde ein hypothetischer doppelter Finalize einen Fehler verursachen.
      Es gibt Fälle in denen ReleaseComObejct nicht gut genug ist. Daher weist auch MS darauf hin FinalReleaseComObject zu verwenden um super sicher zu sein.
      Danke @ErfinderDesRades

      Viele Grüße

      Dieser Beitrag wurde bereits 8 mal editiert, zuletzt von „Haudruferzappeltnoch“ ()