Das Dispose-Pattern zur Ressourcenfreigabe in .NET

Das .NET Framework unterscheidet zwischen Referenztypen, die immer auf dem Heap organisiert werden, und den (normalerweise) auf dem Stack abgelegten Wertetypen. Heap-Objekte werden automatisch durch den Garbage Collector freigegeben.

Benutzt eine Klasse beispielsweise unverwaltete Ressourcen, ist die alleinige Freigabe dieser Ressourcen durch den Destruktor ungeeignet, da dieser ausschließlich implizit vom Garbage Collector aufgerufen wird.

Durch das Dispose-Pattern wurde eine Möglichkeit geschaffen, Ressourcen explizit und damit kontrolliert freizugeben.

Einsatzgebiete des Dispose-Patterns

Das Dispose-Pattern muss für Klassen implementiert werden, wenn mindestens eine der folgenden Bedingungen erfüllt ist:

  • Klasse ist Eigentümer von Objekten, die ebenfalls das Dispose-Pattern implementieren
  • Klasse ist Eigentümer von Ressourcen

Das Dispose-Pattern sollte bereits in Basisklassen vorgesehen werden, wenn absehbar ist, dass es für abgeleitete Klassen benötigt werden könnte.

Aufbau des Dispose-Patterns

Der Hauptbestandteil des Dispose-Patterns ist die Funktion Dispose, durch die die Schnittstelle IDisposable implementiert wird. Sie ist für die Ressourcenfreigabe verantwortlich.

Im folgenden Beispiel wird das Dispose-Pattern für eine abstrakte Basisklasse verwendet. Die Funktion für die eigentliche Freigabe ist abstrakt und muss von den abgeleiteten Klassen bereitgestellt werden.

using System;

namespace Liersch.DisposableExamples
{
  public abstract class DisposableObject : IDisposable
  {
    ~DisposableObject()
    {
      Dispose(false);
    }

    public void Dispose()
    {
      GC.SuppressFinalize(this);
      Dispose(true);
    }

    protected abstract void Dispose(bool disposing);
  }
}

Für den Fall, dass keine explizite Freigabe stattfindet, muss die Funktion Dispose vom Destruktor aufgerufen werden.

SuppressFinalize kennzeichnet ein Objekt als freigegeben, so dass kein Destruktor-Aufruf während der Garbage Collection mehr erfolgt. Diese Kennzeichnung erfolgt vor der Freigabe durch die abstrakte Funktion. Dadurch wird sichergestellt, dass im Fehlerfall kein weiterer Aufruf mehr erfolgt.

Wertetypen können zwar die Schnittstelle IDisposable implementieren, jedoch sind keine Destruktoren verfügbar, so dass das Dispose-Pattern nicht vollständig umsetzbar ist.

Explizite und implizite Ressourcenfreigabe

Für Objekte, die das Dispose-Pattern implementieren, sollte immer eine explizite Freigabe erfolgen, sobald sie nicht mehr benötigt werden. Andernfalls erfolgt die Freigabe durch den Garbage Collector zu einem nicht vorhersehbaren Zeitpunkt, in nicht vorhersehbarer Reihenfolge und im Kontext eines beliebigen Threads.

Durch eine explizite Freigabe kann erreicht werden, dass Ressourcen nur solange belegt bleiben, wie unbedingt nötig. Außerdem kann eine kontrollierte Freigabe erfolgen. Das heißt, dass Objekte, die Eigentümer anderer freizugebender Objekte sind, diese in einer bestimmten Reihenfolge freigeben können.

Nicht-verwaltete Ressourcen müssen unabhängig davon, ob es sich um einen expliziten oder impliziten Aufruf der Funktion Dispose handelt, immer freigegeben werden. Verwaltete Ressourcen hingegen dürfen nur durch explizite Aufrufe der Funktion Dispose freigegeben werden. Andernfalls verursacht der implizite Aufruf von Dispose durch den Garbage Collector Fehler, da referenzierte Objekte bereits freigegeben worden sein könnten oder gerade durch einen anderen Thread freigegeben werden. Probleme treten dann häufig beim Beenden der Anwendung auf, da zu diesem Zeitpunkt die letzte Garbage Collection stattfindet.

Endgültigkeit der Ressourcenfreigabe

Der Aufruf der Funktion SuppressFinalize stellt sicher, dass für ein explizit freigegebenes Objekt kein Aufruf des Destruktors mehr erfolgt, wenn das Objekt durch den Garbage Collector endgültig entfernt wird. Die Garbage Collection wird dadurch beschleunigt.

Freigegebene Objekte dürfen nicht mehr benutzt werden. Funktionen, die ein bereits freigegebenes Objekt reaktivieren, sind ebenfalls nicht erlaubt, da eine implizite Freigabe aufgrund des bereits erfolgten Aufrufs von SuppressFinalize nicht mehr erfolgen würde.

Ressourcenbelegung im Konstruktor

Die Belegung von Ressourcen innerhalb des Konstruktors birgt einige Risiken, besonders dann, wenn mehrere Ressourcen benötigt werden. Schlägt beispielsweise die zweite Anforderung fehl, erfolgt für die erste angeforderte Ressource keine explizite Freigabe, wenn keine Vorkehrungen in Form einer Ausnahmenbehandlung getroffen wurden.

Die folgende Klasse startet im Konstruktor einen weiteren Thread zur Erledigung einer Aufgabe im Hintergrund. Ein manuelles Ereignis dient zur Steuerung des Threads. Alle im Konstruktor angeforderten Ressourcen werden im Fehlerfall explizit freigegeben.

using System;
using System.Diagnostics;
using System.Threading;

namespace Liersch.DisposableExamples
{
  public sealed class ExampleThread : DisposableObject
  {
    public ExampleThread()
    {
      try
      {
        m_Event=new ManualResetEvent(false);
        m_Thread=new Thread(ThreadFunction);
        m_Thread.IsBackground=true;
        m_Thread.Start();
      }
      catch
      {
        Dispose();
        throw; // Rethrow exception
      }
    }

    protected override void Dispose(bool disposing)
    {
      if(disposing)
      {
        // Indicate thread termination request
        m_Terminated=true;

        // Stop waiting to get a fast feedback
        if(m_Event!=null)
          m_Event.Set();

        // Wait for end of thread
        if(m_Thread!=null && m_Thread.IsAlive)
          m_Thread.Join(); // Don't specify a time-out value here

        // Release resources
        if(m_Event!=null)
          m_Event.Close();
      }

      m_Thread=null;
      m_Event=null;
    }

    void ThreadFunction()
    {
      try
      {
        while(!m_Terminated)
        {
          // Wait a second
          m_Event.WaitOne(1000);

          // Thread termination requested?
          if(m_Terminated)
            break;

          // Do something each second
          DoSomething();
        }
      }
      catch(Exception e)
      {
        Debug.Fail("Thread exception", e.GetType().FullName+" - "+e.Message);
      }
    }

    void DoSomething()
    {
      // ...

      if(m_Terminated)
        return;

      // ...

      if(m_Terminated)
        return;

      // ...

      Debug.WriteLine("Thread example task done");
    }

    bool m_Terminated;
    ManualResetEvent m_Event;
    Thread m_Thread;
  }
}

Wird von einer derartigen Klasse abgeleitet, muss eine analoge Ausnahmenbehandlung für alle Konstruktoren der neuen Klasse realisiert werden. Die spezielle Fehlerbehandlung für Konstruktoren entfällt, wenn keine oder lediglich eine einfache Ressourcenbelegung erfolgt, also danach keine Aktionen folgen, die fehlschlagen könnten.

Zusammenfassung des Dispose-Patterns

  • Ressourcenbelegung möglichst nicht im Konstruktor
  • andernfalls Besonderheiten bei Ressourcenbelegung durch Konstruktor beachten
  • möglichst keine Ausnahmen innerhalb der Funktion Dispose verursachen
  • Objekte nach dem Aufruf von Dispose nicht mehr verwenden
  • Dispose-Pattern nach Möglichkeit nicht zu abgeleiteten Klassen hinzufügen

Ähnliche Artikel

Schreiben Sie einen Kommentar