Wikipedia · einfach zusammengefasst · Stand
Verzögerte Initialisierung
In Multithreading-Code muss der Zugriff auf verzögert-initialisierte Objekte und Status synchronisiert werden, um eine Wettlaufsituation (race condition) zu …
Inhalt4 Abschnitte
Grundidee und Wirkungsweise
Verzögerte Initialisierung ist ein Entwurfsmuster der Computer-Programmierung. Dabei wird die Erstellung eines Objekts, die Berechnung eines Werts oder ein anderer teurer Prozess erst ausgeführt, wenn das Ergebnis erstmals benötigt wird. Sie ist eine besondere Form der verzögerten Auswertung, die sich vor allem auf die Instanziierung von Objekten oder anderen Ressourcen bezieht.
Typischerweise wird eine Zugriffs-Methode erweitert, etwa die Get-Methode einer Eigenschaft, die auch Getter genannt wird. Sie prüft, ob ein privates Member, das als Cache dient, bereits initialisiert wurde. Falls ja, gibt sie den gespeicherten Wert sofort zurück. Falls nein, erstellt sie eine neue Instanz, speichert diese in der Member-Variable und gibt sie just-in-time (JIT), also unmittelbar bei der ersten Verwendung, an den Aufrufer zurück.
Das Muster ist besonders nützlich, wenn Eigenschaften oder Ressourcen eines Objekts selten benötigt werden. Die Startgeschwindigkeit kann dadurch steigen, weil die Initialisierung nicht vollständig in der Startphase des Systems erfolgt. Stattdessen wird der Aufwand zeitlich verteilt und damit „amortisiert“. Die durchschnittliche Programmausführung kann sich etwas verschlechtern, weil zusätzliche Bedingungs-Variablen Speicher benötigen und bei Zugriffen geprüft werden müssen. Die mittleren Antwortzeiten können sich jedoch erheblich verbessern.
In Multithreading-Code müssen sowohl der Zugriff auf verzögert-initialisierte Objekte als auch der zugehörige Status synchronisiert werden. Andernfalls kann eine Wettlaufsituation (race condition) entstehen, bei der mehrere Threads denselben Zustand gleichzeitig prüfen oder verändern.
Die verzögerte Fabrik
In der objektorientierten Sichtweise („OOP View“) wird verzögerte Initialisierung häufig mit einer Fabrikmethode, also einer Methode zum Erzeugen von Klasseninstanzen, verbunden. Die sogenannte träge oder verzögerte Fabrik kombiniert drei Ideen:
- Eine Fabrikmethode erzeugt Instanzen einer Klasse.
- Die erzeugten Instanzen werden in einer Zuordnungstabelle gespeichert. Für eine weitere Anforderung mit denselben Parametern wird dieselbe Instanz zurückgegeben. Dies entspricht dem Multiton-Muster.
- Die Instanz wird erst bei der ersten Anforderung erzeugt. Dadurch wird die Erstellung tatsächlich verzögert.
In den Beispielen werden Früchte anhand ihres Namens oder Typs verwaltet. Wird beispielsweise zuerst „Banane“ angefordert, entsteht eine Instanz und wird gespeichert. Eine spätere Anforderung von „Banane“ liefert diese bereits vorhandene Instanz zurück. Wird dagegen „Apfel“ angefordert, wird dafür eine neue Instanz angelegt.
Umsetzung in verschiedenen Programmiersprachen
Die Beispiele zeigen, dass das Muster in objektorientierten und nicht objektorientierten Sprachen umgesetzt werden kann:
- ActionScript 3, C#, C++, Crystal, Haxe, JavaScript und PHP speichern erzeugte Fruit-Instanzen in einer Tabelle, Map, einem Dictionary oder einem vergleichbaren Objekt. Der Schlüssel ist der Fruchtname oder -typ. Existiert noch kein Eintrag, wird eine neue Instanz erzeugt; danach wird der gespeicherte Eintrag zurückgegeben. Die Beispielausgaben zeigen jeweils, dass die Zahl der Instanzen beim erneuten Anfordern derselben Frucht nicht weiter steigt.
- In C wird die verzögerte Auswertung normalerweise in einer einzelnen Funktion oder Quelldatei mit statischen Variablen umgesetzt. Die Funktion get_fruit durchsucht eine verkettete Liste nach einem Namen. Nur wenn der Name noch nicht vorhanden ist, wird mit malloc Speicher reserviert, der Name kopiert, eine fortlaufende Nummer mit ++seq vergeben und das neue Element in die Liste eingefügt. Die gemeinsam verwendeten statischen Variablen können auch in einer einzelnen Quelldatei liegen und bleiben dadurch vor nicht zugehörigen Funktionen verborgen.
- In C# enthält das .Net-Framework 4.0 die Klasse Lazy. Ein Wert kann beispielsweise mit var lazyFruit = new Lazy<Fruit>(); angelegt und erst durch lazyFruit.Value abgerufen werden. Ein weiteres Beispiel verwendet eine Aufzählung LazyObjectSize mit den Größen None, Small, Big, Bigger und Huge. Die jeweils unterschiedlich große, teure Liste wird erst erzeugt, wenn die entsprechende Größe erstmals angefordert wird.
- Java zeigt neben einer einfachen HashMap-Lösung auch eine Variante für hochgradig nebenläufige Umgebungen. Die einfache Methode prüft, ob ein Typ vorhanden ist, und fügt gegebenenfalls eine neue Instanz ein. Im Artikel wird ausdrücklich darauf hingewiesen, dass diese Variante Nebenläufigkeitsprobleme besitzt und Daten beschädigt werden können. Die zweite Variante verwendet das Muster der doppelten Prüfung mit Sperre („double-checked locking“): Nach einer ersten Prüfung wird synchronisiert und innerhalb der Sperre nochmals geprüft, bevor die Instanz erzeugt wird.
- In Python enthält die Klasse Fruits ein Dictionary items. Bei get_fruit wird nur dann ein Fruit-Objekt erzeugt, wenn der angeforderte Eintrag noch nicht im Dictionary vorhanden ist.
- Ruby verwendet für ein Authentifizierungs-Token von einem entfernten Dienst wie Google den Ausdruck @auth_token ||=. Der Token wird erst beim ersten Aufruf von auth_token abgerufen und anschließend gespeichert. Diese Form des Zwischenspeicherns ist zugleich ein Beispiel für Memoisierung.
- Scala unterstützt verzögerte Initialisierung direkt mit lazy val. Ein gewöhnliches val führt den Block { println("Hello"); 99 } sofort aus. Bei lazy val y = { println("Hello!!"); 31 } erscheint die Ausgabe erst beim ersten Zugriff auf y. Danach liefert jeder weitere Zugriff den bereits berechneten Wert 31.
- Smalltalk zeigt die typische Zugriffs-Methode height: ^height ifNil: [height := 2.0]. Ist height noch nicht gesetzt, wird der Wert 2.0 eingetragen und zurückgegeben. Die nicht verzögerte Alternative setzt height := 2.0 bereits in einer Initialisierungs-Methode beim Erzeugen des Objekts; die Zugriffs-Methode gibt dann nur noch ^height zurück.
Verzögertes Array in der theoretischen Informatik
In der theoretischen Informatik bezeichnet verzögerte Initialisierung, auch verzögertes Array genannt, eine Technik zum Entwerfen von Datenstrukturen, die Speicher verwenden, der nicht zuvor vollständig initialisiert werden muss.
Gegeben sei eine Tabelle T mit n nicht initialisierten Speicherzellen, nummeriert von 1 bis n. Es sollen m Zellen gesetzt werden, beispielsweise durch T[ki] := vi für die Paare (k1, v1), …, (km, vm), wobei alle ki verschieden sind. Ohne verzögerte Initialisierung müssten zunächst alle n Zellen vorbereitet werden. Das würde insgesamt O(m + n)-Operationen erfordern. Mit der Technik sind dagegen nur O(m)-Operationen nötig.
Dazu wird eine zweite Tabelle V angelegt. In ihr werden die Paare (ki, vi) in beliebiger Reihenfolge gespeichert. Für jedes i wird in T[ki] die Position in V eingetragen, an der der Schlüssel ki liegt. Die übrigen Zellen von T bleiben nicht initialisiert.
Bei einer Abfrage von T[k] wird zunächst geprüft, ob k im Bereich {1, ..., m} liegt. Liegt k außerhalb dieses Bereichs, ist T[k] nicht initialisiert. Liegt k innerhalb des Bereichs, wird V[T[k]] betrachtet. Ist die erste Komponente des dort gespeicherten Paars nicht gleich k, gilt T[k] ebenfalls als nicht initialisiert; der zufällige Eintrag in T[k] ist dann lediglich in den Bereich {1, ..., m} gefallen. Stimmen die Schlüssel überein, ist T[k] tatsächlich initialisiert, und die zweite Komponente des Paars enthält den zugehörigen Wert.