Initialisierung verzögern mit Lazy (C#)

Initialisierung verzögern mit Lazy (C#)
Page content

Der Begriff Lazy Loading („träges Laden“) bedeutet, dass ein Objekt bzw. Daten erst beim Zugriff geladen werden. Ich zeige euch, wie ihr es selbst implementieren könnt oder mit System. Lazy schneller und einfacher an euer Ziel kommt :)

Sinn und Zweck von Lazy Loading

Wie eingangs beschrieben bezeichnet Lazy Loading das (Nach-)Laden von Inhalten. Es wird vor allem da eingesetzt, wo das Bereitstellen länger dauern kann. Das Bereitstellen kann verschiedene Gründe haben:

  • Aufwändiges rendern einer Benutzeroberfläche
  • Lesen vieler Daten aus einer ggf. langsamen Quelle
  • Anwendungen beschleunigen oder responsiver machen

In jedem Fall wird eine Aktion verzögert durchgeführt und spätestens dann, wenn der erste Zugriff erfolgt. Dies kann auch den Vorteil haben, dass eine aufwändige Bereitstellung nicht erfolgen muss, wenn niemand die Funktion aufruft.

Lazy Loading selbst implementieren

In folgendem Beispiel gehen wir davon aus, dass wir eine Klasse erstellen wollen, dessen Konstruktor länger braucht. Eine einfache Implementierung kann folgendermaßen aussehen:

private static MyClass _instance;

public static MyClass Instance {
  get {
    if (_instance == null) {
      // Erstellung einer Instanz stellvertretend für eine
      // Aktion, die lange dauert
      _instance = new MyClass();
    }

    return _instance;
  }
}

Die Funktion prüft erst, ob eine Instanz vorhanden ist. Ist dies nicht der Fall, wird eine Instanz erstellt und gespeichert. Der erste Zugriff dauert etwas länger. Alle nachfolgenden Aufrufe erhalten die vorher erstellte Instanz.

Diese Implementierung reicht wahrscheinlich für viele Anwendung aus. Allerdings kann es zu Fehlern kommen, wenn zwei Threads parallel auf diese Instanz zugreifen. Im schlimmsten Falle wird die Instanz zwei Mal erstellt und erzeugt vielleicht einen Ressourcenkonflikt und eine unbehandelte Ausnahme.

Dies lässt sich mit lock beheben:

private static MyClass _instance;
private static object _instanceLock = new object();

public static MyClass Instance {
  get {
    lock (_instanceLock) {
      if (_instance == null) {
        _instance = new MyClass();
      }
    }

    return _instance;
  }
}

In dieser einfachen Form wird die Instanz garantiert nur 1x erstellt. Wenn allerdings viele parallele Threads auf die Instanz zugreifen wird, aufgrund des locks, jeder Thread einzeln verarbeitet. Dies kann in manchen Situationen die Anwendung ausbremsen und zu Performanceproblemen führen.

Eine bessere Lösung kann wie folgt aussehen:

private static MyClass _instance;
private static object _instanceLock = new object();

public static MyClass Instance {
  get {
    if (_instance == null) {
      lock (_instanceLock) {
        if (_instance == null) {
          _instance = new MyClass();
        }
      }
    }

    return _instance;
  }
}

Dadurch, dass lock eine Ebene nach innen verschoben wurde, werden nicht mehr alle Threads blockiert, nachdem die Instanz erstellt wurde. Innerhalb des Aufrufs ist eine zusätzliche Abfrage notwendig, da potentiell zwei oder mehr Threads parallel die Existenz der Instanz geprüft haben und nun eine Instanz erstellen möchten.

Vereinfachung mit System.Lazy

Mit System.Lazy gibt es bereits eine Klasse, welche die o.g. Logik beinhaltet und weitere Zusatzfunktionen bietet. Die Verwendung ist dabei recht einfach:

// Lazy mit dem Standardkonstruktor
public static Lazy<MyClass> InstanceLazy = new Lazy<MyClass>();

// Lazy mit eigener Initialisierungsmethode
public static Lazy<MyClass> InstanceLazy = new Lazy<MyClass>(() => {
  return  new MyClass();
});


// Verwendung im Code. Die Variable instance beinhaltet die Instanz.
MyClass instance = InstanceLazy.Value;

Nur die Erstellung ist Threadsafe

In beiden Versionen ist nur die Erstellung der Instanz Threadsafe. Es kann vorkommen, dass zwei Threads parallel Funktionen der Instanz aufrufen oder Werte manipulieren.

Die Methoden der Klasse müssen separat Threadsafe implementiert oder deren Aufruf durch lock gesteuert werden. Das wird durch die oben stehenden Implementierungen nicht abgedeckt.

Fazit

Wie ihr sehen könnt ist das Lazy-Loading zum Erstellen einer Instanz recht schnell selbst implementiert. Wenn eine Instanz möglicherweise durch viele Threads parallel erstellt wird, solltet ihr System.Lazy verwenden. Dadurch stellt ihr sicher, dass wirklich nur eine Instanz erzeugt wird.


Bildnachweis: Pixabay.com

Weiterführende Links: