Mit der IDisposable-Schnittstelle stellt .NET Framework eine Implementierungsvorschrift bereit, mit deren Hilfe Sie eine Methode implementieren können, die anders als Finalize auch aus Ihrem Code heraus aufgerufen werden darf, um das Objekt zu entsorgen. Wenn Sie die Schnittstelle per Implements in Ihrer Klasse implementieren, müssen Sie die Dispose-Methode einfügen, die dann für das notwendige Aufräumen die Verantwortung trägt. Damit karikiert man natürlich die Funktion eines echten Destruktors in anderen OOP- Sprachen, der eben automatisch (man braucht sich halt nicht darum zu kümmern) aufgerufen wird. Der Aufruf von Dispose ist daher ein Kompromiss, den man für die vielen Vorteile des Garbage Collectors eingeht.

Hinweis: Verwenden Sie Using als Strukturblock, um ein implizites Dispose für das verwendete Objekt zu erreichen, wenn das Programm den Gültigkeitsbereich des Strukturblocks verlässt.

Diese Aufräumarbeiten sind prinzipiell die gleichen Arbeiten, die auch eine Finalize-Methode durchführt. Doch Dispose hat ein wenig mehr Arbeit. Denn wenn Sie Aufräumarbeiten mit Dispose vorgenommen haben, dann müssen Sie dafür sorgen, dass der GC die Aufräumarbeiten innerhalb Ihres Objekts nicht noch einmal durch den Aufruf dieser Methode erledigt. Zu diesem Zweck gibt es die SuppressFinalize-Methode des Garbage Collectors. Rufen Sie diese Methode auf und übergeben Sie ihr Ihre Klasse als Argument, schließt der Garbage Collector sie für alle folgenden GC-Durchläufe von Aufräumarbeiten dieser Art aus. Nun besteht die eigentliche Aufgabe der Dispose-Methode darin, zu unterscheiden, ob ein Aufruf durch das eigene Programm eben über Dispose (üblich ist bei bestimmten Klassen auch Close, das nichts anderes macht als Dispose, nur dass es einen anderen Namen trägt ) oder durch den Garbage Collector über Finalize erfolgt ist. Ihr Dispose muss ebenfalls dafür sorgen, dass erkannt wird, ob ein Objekt aus Ihrer Sicht schon entsorgt wurde, und im Bedarfsfall eine Ausnahme auslösen. Ein Beispiel für die Implementierung einer vollständigen Finalize/Dispose-Lösung finden Sie in der folgen-den Beispielanwendung:

Implementierung eines hochauflösenden Timers als IDisposable

Computer, mit denen Sie in der heutigen Zeit programmieren, sind alles andere als langsam. Der Computer zum Beispiel, auf dem ich gerade diese Zeilen schreibe, ausgestattet mit einem Intel I5 mit 2,67 MHz Takt-frequenz, vermag in einer Sekunde in etwa 50.000 so genannter MIPS durchführen (Million Instructions per Second), und das ergibt 50 Milliarden Anweisungen (Prozessoranweisungen natürlich, keine Basic-Befehle) pro Sekunde. Ein kleines Programm, das obendrein nur einen der vier Prozessorkerne eines Intel I5-Prozessors nutzt, um von 0 bis 4 Milliarden zu zählen, benötigt deswegen dazu auch nur rund 11 Sekunden. Das sind rund 360 Millionen Zählungen pro Sekunde oder 360.000 Zählungen pro Millisekunde:

Dim sw = Stopwatch.StartNew
For c As UInteger = 0 To UInteger.MaxValue – 1
Next
sw.Stop()
MessageBox.Show("Dauer in ms: " & sw.ElapsedMilliseconds.ToString("#,##0"))

Und trotz dieser enormen Rechenleistung erlaubt es uns .NET mit Boardmitteln nicht, auch nur jede Hundertstelsekunde irgendeinen Zustand abzufragen oder eine bestimmte Aufgabe auszuführen.

 

Überlegen wir mal: Eine Hundertstelsekunde, das ist eine Ewigkeit für unseren Prozessor: In 10 ms kann der nämlich rund 500.000.000 Anweisungen verarbeiten – unsere Zählschleife hätte in der Zeit mal eben von 0 bis 3,6 Millionen gezählt. Leistung wäre also genug da, und trotzdem gibt es bei der gestellten Aufgabe ein Problem: Wenn Sie die Zustandsaktualisierungen oder Zustandsabfragen in einer Endlosschleife ausführen, dann blockieren Sie natürlich die Leistung eines kompletten Prozessorkerns. Ein Programm mit der Aufgabe, auf einem Formular (WinForms) oder einem Fenster (WPF) eine Stoppuhr mit Hundertstelsekundenanzeige zu realisieren, sähe wie in folgender Abbildung gezeigt aus.

 

Die einfachste Version unserer Stoppuhr schießt mit Kanonen auf Spatzen: Durch die Endlosschleife beansprucht das Programm die Leistung eines kompletten Prozessorkerns – effizient ist was anderes.

Wenn Sie das Programm starten, sehen Sie das Problem mit einem Blick auf den Task-Manager von Windows oder – wie in der Abbildung zu sehen – eine der netten verfügbaren Prozessorauslastungs-Mini-anwen¬dungen, die Sie sich herunterladen laden können: Das Programm verbraucht nicht weniger als 100% der Leistung eines kompletten Prozessorkerns, da es die Anzeige in einer Endlosschleife aktualisiert und nur zwischendurch DoEvents aufruft, um der Anwendung Zeit zum Luftholen zu geben und ihr es damit ermög-licht, Steuerelemente zu aktualisieren und Abfragen zu verarbeiten. Wenn man auf diese Weise Software entwickelt, kann man damit natürlich keinen Blumentopf gewinnen:

Die Beispieldateien zum folgenden Listing können hier heruntergeladen werden.

 

Public Class Form1

    Private myStopSignaled As Boolean

    Private Sub btnStartStopButton_Click(ByVal sender As System.Object, 
                      ByVal e As System.EventArgs) Handles btnStartStopButton.Click

        If btnStartStopButton.Text = "Start" Then
            btnStartStopButton.Text = "Stop"
            Dim sw = Stopwatch.StartNew

            Dim lastMs As Long

            Do
                'Warten auf die nächste Millisekunde
                Do
                    If lastMs <> sw.ElapsedMilliseconds Then
                        lastMs = sw.ElapsedMilliseconds
                        elapsedMillisecondsLabel.Text =
                            TimeSpan.FromMilliseconds(lastMs).ToString("hh\:mm\:ss\:fff")
                        My.Application.DoEvents()
                        If myStopSignaled Then
                            Return
                        End If
                    End If
                Loop
            Loop
        Else
            myStopSignaled = True
            btnStartStopButton.Text = "Stop"
        End If
    End Sub
End Class

 

»Kein Problem«, wird man sich wohl zunächst denken, »dann realisieren wir das eben mit einem Timer, den wir auf einen 1-ms-Zyklus einstellen!«. Das sollte dann so gut wie keine Prozessorleistung mehr kosten.  O.k., dann schauen Sie sich mal an, was passiert, wenn Sie dem Beispiel eine weitere Schaltfläche mit folgendem Code hinzufügen:

Private myTimer As Timer

    Private Sub TimerTestButton_Click(ByVal sender As System.Object, 
                                      ByVal e As System.EventArgs) Handles TimerTestButton.Click
        myTimer = New Timer
        myTimer.Interval = 1
        myTimer.Start()

        Dim sw = Stopwatch.StartNew
        Dim lastms = sw.ElapsedMilliseconds

        AddHandler myTimer.Tick, Sub(timerSender As Object, timer_e As EventArgs)
                                     Dim cms = sw.ElapsedMilliseconds
                                     Debug.Print("Vergangene Zeit: " & cms - lastms & " ms.")
                                     lastms = cms
                                 End Sub
    End Sub

 

Wenn Sie anschließend das Programm starten und auf die neue Schaltfläche klicken, dann sehen Sie in etwa die folgende Auflistung im Ausgabefenster von Visual Studio (das Sie im Bedarfsfall mit (Strg)+(Alt)+(O) anzeigen lassen können):

Vergangene Zeit: 16 ms.
Vergangene Zeit: 20 ms.
Vergangene Zeit: 13 ms.
Vergangene Zeit: 16 ms.
Vergangene Zeit: 20 ms.
Vergangene Zeit: 12 ms.
Vergangene Zeit: 21 ms.
Vergangene Zeit: 12 ms.
Vergangene Zeit: 14 ms.
Vergangene Zeit: 20 ms.
Vergangene Zeit: 12 ms.
Vergangene Zeit: 14 ms.
Vergangene Zeit: 20 ms.
Vergangene Zeit: 20 ms. 

Zwar wird jetzt kaum noch Prozessorleistung benötigt (davon können Sie sich über den Task-Manager überzeugen), aber das Ziel ist verfehlt. Denn der Aufruf zur Aktualisierung der Stoppuhr erfolgt nun erheb-lich seltener als jede Hundertstelsekunde (10 ms), das Intervall liegt deutlich darüber. Sie beobachten damit gerade ein fundamentales Problem eines Nicht-Echtzeit-Betriebssystems, wie Windows eines ist: Sie können den Timer im Beispiel einstellen, wie Sie wollen: Unter 15-20 Millisekunden ist unter Windows 7 nichts zu machen, auf älteren Systemen können schon 25-30 Millisekunden zum Problem werden. Und Ausreißer bis zu 40 ms kommen auf allen Systemen vor. Das liegt nicht zuletzt daran, dass der Scheduler von Windows, der dafür sorgt, dass jeder gleichzeitig laufende Thread im System auch mal zum Zug kommt, ebenfalls keine kleinere Auflösung hat.

Immerhin ist ein Kontextwechsel für einen Thread eine aufwändige Sache – ein Thread unter Windows beansprucht nicht weniger als ein MB, und schon im Normalzustand von Windows, also wenn es gerade gestartet wurde, können mehrere Hundert Threads gestartet werden; wenn sie auch nicht alle aktiv sind

Mit Outlook 2010, Word 2010 und Visual Studio 2010 hat selbst ein modernes Dual Core-Hyper-threading-Netbook schon ausreichend zu tun – nicht weniger als rund 800 Threads werkeln dabei mehr oder weniger aktiv vor sich hin!

 

Deswegen schaltet der Scheduler von Windows zwischen den Threads auch nicht so oft um – so alle 15-20 ms teilen sich die jeweils nächsten von Hunderten gleichzeitig laufender Threads die verfügbaren Prozessorkerne, und jeder, der will, kommt so mal dran. Aber eben auch nicht öfter. Damit kommen weder die normalen, in .NET zur Verfügung stehenden Timer noch sonst ein anderer Trick in Frage, ohne eine Auflösung von unter 10 ms hinzubekommen, bei der nicht die Rechenleistung eines kompletten Kerns verplempert wird.

 

Eine Lösung bleibt jedoch noch: Windows bietet schon seit Windows XP einen so genannten Multimediatimer an, der in der Regel zum Synchronisieren von Wiedergabemedien (Audio, Video) verwendet wird. Auch hier wird eine höhere Auflösung benötigt, als sie die normalen Timer zur Verfügung stellen könnten; der Multimediatimer hat deshalb auch eine Auflösung von 1 ms. Da es .NET erlaubt, auch Betriebssystemfunktionen durch so genannte Plattform Invoke-Aufrufe zu nutzen (P/Invoke, sprich »Pieh Inwouk«), lässt sich mit relativ wenig Aufwand eine Klasse entwickeln, die einen solchen Timer auch in .NET zur Verfügung stellt:

Die Beispieldateien können hier heruntergeladen werden.

Die Klasse, die den Multimediatimer zur Verfügung stellt, sieht folgendermaßen aus:

Imports System.ComponentModel
Imports System.Runtime.InteropServices

Public Class HighSpeedTimer

    Private myTimerID As Integer                ' ID des Multimedia-Timers
    Private myUser As Integer                   ' Benuzerdefinierter Parameter, nicht verwendet.
    Private myTimerCapabilities As TimerCaps    ' Hält die TimeCaps, die die Timerauflösung beschreiben
    Private myMode As Integer = 1               ' 0=Einmalig, 1=Periodisch. 1 wird vordefiniert.

    Private myPeriodInMs As Integer             ' Wie oft soll getriggert werden?
    Private myresolutionInMs As Integer         ' Welche Auflösung kann der Timer?
    Private myHasStarted As Boolean             ' Läuft der Timer?

    'Delegate, der für den Rückruf aus dem Betriebssystem benötigt wird,
    'wenn der Timer abgelaufen ist...
    Private Delegate Sub TimeProc(ByVal id As Integer, ByVal msg As Integer,
                                  ByVal user As Integer, ByVal reserved1 As Integer,
                                  ByVal reserved2 As Integer)

    '...und in dieser Delegatenvariable definiert wird.
    Private myCallBackTimeProc As TimeProc

    'Das Ereignis, was durch die Rückrufroutine ausgelöst wird,
    'wenn der Timer abgelaufen ist.
    Public Event Elapsed(ByVal sender As Object, ByVal e As EventArgs)

 

 

Die folgenden Zeilen zeigen, wie Sie von Visual Basic aus Methoden des Betriebssystems direkt aufrufen können. Die Methoden für die Steuerung des Multimediatimers befinden sich in der Library winmm.dll (Abkürzung für Windows Multimedia). Das DllImport-Attribut, das sich oberhalb der Methode befindet, gibt an, dass sich der Code der Methode nicht im Methodenrumpf befindet, sondern dass es sich um einen externen Aufruf handelt.

'Ermittelt die Fähigkeiten des Timers auf diesem Rechner.
    <DllImport("winmm.dll")>
    Private Shared Function timeGetDevCaps(ByRef caps As TimerCaps,
                                           ByVal sizeOfTimerCaps As Integer) As Integer
    End Function

    'Erstellt und startet den Multimedia-Timer.
    <DllImport("winmm.dll")>
    Private Shared Function timeSetEvent(ByVal delay As Integer, ByVal resolution As Integer,
                                         ByVal CallBackProc As TimeProc, ByVal user As Integer,
                                         ByVal mode As Integer) As Integer
    End Function

    'Löscht einen Timer, der gerade in Betrieb ist.
    <DllImport("winmm.dll")>
    Private Shared Function timeKillEvent(ByVal id As Integer) As Integer
    End Function

    ''' <summary>
    ''' Erstellt eine Instanz dieser Klasse und setzt Wiederholungsfrequenz 
    ''' des Timer-Triggers und Auflösung in Millisekunden.
    ''' </summary>
    ''' <param name="periodInMs">Wiederholungsfrequenz - wie oft soll der Timer auslösen.</param>
    ''' <param name="resolutionInMs">Auflösung des Timers (der Wert 1ms liefert 
    ''' die genauesten Ergebnisse).</param>
    ''' <remarks></remarks>
    Sub New(ByVal periodInMs As Integer, ByVal resolutionInMs As Integer)
        'Ermittelt die Fähigkeiten des Multimedia Timers, die dann über 
        'TimerCapabilities aubrufbar sind.
        Dim ret = timeGetDevCaps(myTimerCapabilities, Marshal.SizeOf(myTimerCapabilities))

        myPeriodInMs = periodInMs
        myresolutionInMs = resolutionInMs
        myCallBackTimeProc = AddressOf CallBackTimeProc
    End Sub

    ''' <summary>
    ''' Startet den Timer.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub Start()

        If myHasStarted Then
            Return
        End If

        Dim ret = timeSetEvent(myPeriodInMs, myresolutionInMs,
                               myCallBackTimeProc, myUser, myMode)
        Dim hr = Marshal.GetHRForLastWin32Error
        If ret = 0 Then
            Throw New Win32Exception("Der Timer konnte nicht gestartet werden!")
        Else
            myTimerID = ret
            myHasStarted = True
        End If

    End Sub

 

Die folgende Methode ist nun die Methode, die uns Kopfzerbrechen machen sollte. Solange ein Programm, das die Multimediatimer-Klasse verwendet, immer dafür sorgt, dass der Timer gestoppt wird, kann nichts passieren. Aber was geschieht, wenn das Programm auf unvorhersehbare Ereignisse stößt, die dazu führen, dass der Timer nicht freigegeben werden kann? Unter Umständen läuft der Timer dann noch zu einem Zeitpunkt weiter, an dem Sie als Entwickler längst nicht mehr damit rechnen, oder schlimmer: Gegebenenfalls kann das Programm sogar beendet werden, ohne dass es die Möglichkeit hatte, den angeforderten Timer anzuhalten und wieder freizugeben! Um das zu verhindern, dient die Implementierung von IDisposable, und der nächste Abschnitt zeigt, wie das funktioniert.

''' <summary>
    ''' Stoppt den Timer.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub [Stop]()
        If Not myHasStarted Then
            Return
        End If

        timeKillEvent(myTimerID)
    End Sub

    'Die Rückrufroutine, die vom Betriebssystem aufgerufen wird, 
    'wenn der Timer abgelaufen ist.
    Private Sub CallBackTimeProc(ByVal id As Integer, ByVal msg As Integer,
                                 ByVal user As Integer, ByVal param1 As Integer,
                                 ByVal param2 As Integer)
        RaiseEvent Elapsed(Me, EventArgs.Empty)
    End Sub

    ''' <summary>
    ''' Ermittelt die Möglichkeiten des Systems bezüglich des Timers.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property TimerCapabilities As TimerCaps
        Get
            Return myTimerCapabilities
        End Get
    End Property

    ''' <summary>
    ''' Ermittelt, ob der Timer gestartet wurde.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property HasStarted As Boolean
        Get
            Return myHasStarted
        End Get
    End Property

End Class

'Wird benötigt für die Parameterübergabe an die Betriebssystemroutinen
<StructLayout(LayoutKind.Sequential)>
Public Structure TimerCaps
    Public PeriodMin As Integer
    Public periodMax As Integer
End Structure

 

Unterstützung durch den Visual Basic-Editor beim Einfügen eines Disposable-Patterns

Das Muster, auf das Sie beim Implementieren einer »disposebaren« Klasse achten müssen, ist nicht nur etwas komplexer als bei der Implementierung anderer Funktionen, die Sie durch die Einbindung einer oder mehrerer Schnittstellen implementieren müssen.

 

Sie müssen auch darauf achten, dass ein internes Muster strikt einzuhalten ist – und das geht natürlich weit über das bloße Vorhandensein einer entsprechenden Dispose-Methode hinaus.

 

Aus diesem Grund gibt es eine besondere Editorunterstützung für die IDisposable-Schnittstelle. Sobald Sie diese am Klassenkopf einfügen und nach Implements IDisposable (¢) betätigen, fügt der Editor nicht nur die Funktionsrümpfe, die das Interface vorschreibt, sondern ein etwas spezifischeres Funktionsgerüst als Code in Ihre Klasse ein.

 

Sie können das direkt am besprochenen Highspeed-Timer-Beispiel ausprobieren. Nachdem Sie die oben beschriebenen Schritte durchgeführt haben, befindet sich fast die komplett fertige IDisposable-Implemen­tierung bereits am Ende der Klasse HighSpeedTimer, wie in folgender Abbildung zu sehen.


 

 

Die Ergänzungen, die wir an dieser Stelle noch vornehmen müssen, sind schnell gemacht. Zunächst folgen wir den Vorschlägen in den Kommentaren oberhalb der Methode Finalize. Da es sich bei unserem Timer tatsächlich um eine nicht verwaltete (unmanaged) Ressource handelt, nehmen wir die Auskommentierung der betroffenen Zeilen, wie in der Grafik zu sehen, zurück. Damit stellen wir sicher, dass die Dispose-Methode, die wir gerade implementieren, auch über die Finalize-Methode über den Garbage Collector im Bedarfsfall aufgerufen wird.

 

Und schließlich brauchen wir lediglich dafür zu sorgen, an entscheidender Stelle den Timer, falls er noch aktiv ist, wieder freizugeben, und dieser Code ist schnell implementiert (die Änderungen finden Sie in fetter Schrift im folgenden Listing):

#Region "IDisposable Support"
    Private disposedValue As Boolean ' So ermitteln Sie überflüssige Aufrufe

    ' IDisposable
    Protected Overridable Sub Dispose(ByVal disposing As Boolean)
        If Not Me.disposedValue Then
            'Wir haben keine verwalteten Objekte, die wir berücksichtigen müssten,
            'deswegen brauchen wir diesen If-Zweig nicht:
            'If disposing Then
            '    ' TODO: Verwalteten Zustand löschen (verwaltete Objekte).
            'End If

            'Hier muss der Timer freigegeben werden, falls er noch aktiv ist.
            If myHasStarted Then
                Me.Stop()
            End If

        End If
        Me.disposedValue = True
    End Sub

    ' TODO: Finalize() nur überschreiben, wenn Dispose(ByVal disposing As Boolean) 
    ' oben über Code zum Freigeben von nicht verwalteten Ressourcen verfügt.
    Protected Overrides Sub Finalize()
        ' Ändern Sie diesen Code nicht. Fügen Sie oben in 
        ' Dispose(ByVal disposing As Boolean) Bereinigungscode ein.
        Dispose(False)
        MyBase.Finalize()
    End Sub

    ' Dieser Code wird von Visual Basic hinzugefügt, um das Dispose-Muster richtig zu implementieren.
    Public Sub Dispose() Implements IDisposable.Dispose
        ' Ändern Sie diesen Code nicht. Fügen Sie oben in 
        ' Dispose(ByVal disposing As Boolean) Bereinigungscode ein.
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

 

Was an dieser Stelle noch wichtig zu erwähnen ist, betrifft die letzte Zeile in der Dispose-Methode. Der Entwickler, der unsere HighSpeedTimer-Klasse verwendet, ist nun in der Lage, das Objekt manuell durch den Aufruf von Dispose zur Entsorgung freizugeben. Wenn das allerdings passiert, müssen die Aufräumarbeiten nicht mehr vom Garbage Collector ausgeführt werden, ja das Gegenteil ist sogar der Fall: Der Garbage Collector darf sogar in einem solchen Fall den Aufräumcode nicht mehr durchlaufen und muss deswegen darüber informiert werden, dass diese Arbeiten bereits durchgeführt wurden. Das geschieht mit der letzten Zeile des Listings, mit der Methode GC.SuppressFinalize.

Die so geänderten Begleitdateien können Sie hier herunterladen

 

Mehr zu diesem Thema in: Visual Basic 2010 das Entwicklerbuch von Klaus Löffelmann, erhältlich bei Amazon auch auf Englisch , und bei Microsoft Press. Möchten Sie Klaus Löffelmann oder das ActiveDevelop-Team für professionelle Software-Entwicklungsprojekte (Migration, C#, Visual Basic.NET) buchen, informieren Sie sich auf www.activedevelop.de

klauslo am 04. Dezember 2011

Tags: , , , , | Categories: Garbage Collector