Net-Base Rivista

18.10.2026

Generics: Thread-sicherer Cache mit TMonitor und Double-Checked Locking – wo es in Delphi schiefgeht

Double-Checked Locking klingt nach einer sauberen Cache-Optimierung. In Delphi führt es bei generischen Caches jedoch schnell zu seltenen, lastabhängigen Fehlern. Dieser Irrtum-Check zeigt den kritischen Randfall – und robuste Alternativen für Betrieb und Wartung.

18.10.2026

Dal tema della rivista alla pratica di progetto

Pagine di servizi e tecniche correlate all'articolo

Montag, 09:12 Uhr: Ein Service läuft seit Wochen unauffällig. Dann kommt ein Lastpeak, ein neuer Mandant, ein neuer Datenstand – und plötzlich steht in der Ereignisanzeige eine sporadische Access Violation. Nicht ständig. Nicht reproduzierbar auf Anhieb. Nach Neustart: wieder ruhig. Im Code findet sich eine „clevere“ Stelle, die eigentlich alles verbessern sollte: Double-Checked Locking im generischen Cache. Der Satz dazu klingt vertraut: „Wenn der Cache warm ist, sparen wir uns den Lock.“

Der Irrtum daran: Double-Checked Locking macht den thread-sicheren Cache in Delphi nicht automatisch schneller und korrekt. Es macht ihn oft nur schwieriger zu beweisen – und im Fehlerfall deutlich schwerer zu debuggen. Dieser Irrtum-Check zeigt, wo es in Delphi mit System.TMonitor konkret schiefgeht, welche Randfälle in generischen Caches wirklich zählen und welche Muster sich in der Praxis besser betreiben lassen.

thread-sicherer Cache in Delphi: Irrtum-Check „DCL + TMonitor ist doch Standard“

Mess- und Diagnose-Setup mit Notizblock und unscharfen Zeitreihen zur Analyse von Thread-Last und Cache-Zugriffen.
Debugging unter Last: Telemetrie statt Breakpoints.
Arbeitsplatzszene mit einem textfreien Diagramm zum Cache-Pre-Warming und parallelen Requests.
Pre-Warming reduziert Contention, ohne DCL-Risiko.
Abstrakte Darstellung: paralleler ungesperrter Lesezugriff kollidiert mit einem Schreibzugriff auf ein Dictionary.
DCL-Schwachstelle: ungesperrter Read trifft parallelen Write am Dictionary.

Double-Checked Locking (DCL) folgt einer einfachen Idee: Erst ohne Lock prüfen, ob der Cache-Eintrag existiert. Nur wenn nicht, wird gelockt, erneut geprüft und dann gebaut und abgelegt. Damit sollen teure Locks im „Warmzustand“ verschwinden.

Das Problem ist nicht die Idee, sondern die Voraussetzung. DCL ist nur dann korrekt, wenn sichergestellt ist, dass ein Thread nicht eine Referenz auf ein Objekt sieht, dessen Initialisierung noch nicht vollständig sichtbar ist. Ohne definierte Speicher-Schranken (Memory Barriers), volatile-Semantik oder Atomics kann Reordering/Visibility dazu führen, dass ein anderer Thread „zu früh“ etwas als vorhanden betrachtet.[Quelle] Die Error-Prone-Dokumentation ist Java-zentriert, aber der Kern ist sprachübergreifend: DCL hängt an klaren Regeln des Memory Models, nicht am Bauchgefühl.

Für Delphi kommt ein zweiter Punkt hinzu: System.TMonitor ist als Monitor-Mechanismus (Enter/Exit/TryEnter) plus Condition-Variable-Verhalten (Wait/Pulse/PulseAll) dokumentiert.[Quelle] Das hilft beim korrekten Warten und Signalisieren. Was die DocWiki in dieser Form aber nicht liefert, ist eine explizite Zusage, dass damit jede Form von DCL-Publikation ohne zusätzliche Maßnahmen sicher wäre. Diese Lücke ist praktisch relevant: Wer DCL einsetzt, muss Korrektheit argumentieren können – nicht nur Performance hoffen.

Delphi-spezifischer Stolperstein: TMonitor ist nicht immer der Monitor

Bevor man überhaupt über Locks diskutiert, lohnt sich ein kurzer Reality-Check: In Delphi gibt es zwei prominente TMonitor-Typen mit völlig unterschiedlicher Bedeutung. System.TMonitor ist der Threading-Monitor. Vcl.Forms.TMonitor beschreibt einen Bildschirm-Monitor im Multi-Monitor-Setup.[Quelle]

Das klingt nach einer Anfängerfalle, taucht aber auch in großen Codebasen auf: Units werden nachgezogen, Namen werden in Diskussionen verkürzt, jemand sagt „wir nutzen TMonitor“, und plötzlich meint die Hälfte etwas anderes. Praktischer Gegenmaßstab: Wenn es um Synchronisation geht, sollte im Code (oder in Architektur-Notizen) eindeutig System.TMonitor stehen – notfalls vollqualifiziert, damit es keine Debatte gibt.

Wo Double-Checked Locking in Delphi wirklich knallt

Wenn DCL schiefgeht, sieht es selten nach „klassischem“ Synchronisationsfehler aus. Es sieht nach zufälligem Chaos aus. Gerade bei generischen Caches ist das gefährlich, weil der Cache dann in vielen Modulen steckt und Fehler über weite Strecken propagiert.

1) Sichtbarkeit und Teilinitialisierung: Referenz „da“, Inhalt nicht stabil

Der typische DCL-Schaden: Thread A baut ein Objekt, schreibt die Referenz irgendwo hin (Cache, Feld, Dictionary-Value). Thread B prüft ohne Lock, sieht „da“ und nutzt es. Wenn A zwar die Referenz sichtbar gemacht hat, aber nicht garantiert ist, dass alle initialisierten Felder, internen Listen oder abhängigen Strukturen für B bereits sichtbar sind, entsteht ein Zustand, der logisch unmöglich wirkt: Ein Objekt existiert, aber ein internes Feld ist noch Default oder nil. Genau das ist der Kern der DCL-Warnungen: Ohne Memory-Model-Garantien kann ein anderer Thread eine Referenz sehen, bevor die Initialisierung vollständig „durchpubliziert“ ist.[Quelle]

Warum ist das so unangenehm? Weil der Crash oft nicht im Cache passiert. Er passiert später – beim Zugriff auf eine eigentlich garantierte Eigenschaft. Dann zeigt der Stacktrace auf ganz andere Bereiche (Mapper, Serializer, Datenzugriff), und die Synchronisationsstelle wirkt unschuldig.

2) Container-Sicherheit: TDictionary ist kein paralleler Read-Container bei concurrent Writes

Viele DCL-Varianten fokussieren auf „Objektaufbau“. Sie vergessen den Container. Ein TDictionary ist nicht darauf ausgelegt, dass ein Thread schreibt (Add/Insert/Resize/Rehash), während ein anderer Thread gleichzeitig ohne Lock liest (TryGetValue/ContainsKey). Selbst wenn der Value-Aufbau perfekt publiziert wäre, kann schon der Dictionary-Interna-Zugriff kollidieren, etwa während Rehash/Resize. DCL wird damit nicht nur ein Visibility-Thema, sondern ein handfester Datenstruktur-Konflikt.

Das ist der Moment, in dem DCL besonders tückisch wird: Der ungesperrte First-Check ist nicht nur „optimiert“, er ist unter bestimmten Umständen schlicht ein unerlaubter Zugriff auf einen mutierenden Container.

3) „Im Worst Case wird zweimal gebaut“ ist selten harmlos

DCL wird oft mit einem Schulterzucken verteidigt: „Dann baut es halt zweimal, aber am Ende liegt es einmal im Cache.“ Das stimmt nur, wenn der Builder idempotent ist und keine externen Effekte hat. In realen Codebasen bauen Caches aber häufig Dinge wie vorbereitete Statements, RTTI-Metadaten, Parser, Mapper, Konfigurationen, HTTP-Clients oder File-basierte Ressourcen. Doppelte Initialisierung kann dann Ressourcen verdoppeln, Zustände doppelt registrieren oder Timingfenster erzeugen, die bei bestimmten Betriebszuständen eskalieren.

4) Reentrancy und Lock-Ordnung: Builder ruft wieder in den Cache

Ein weiterer Randfall, der in Reviews gern untergeht: Der Builder für Key X ruft indirekt Code auf, der wiederum denselben Cache (oder einen zweiten Cache) nutzt. Wenn Lock-Reihenfolgen nicht stabil sind, entstehen Deadlocks oder langwierige Kettenblockaden. DCL verschärft das, weil man schnell zusätzliche Locks einführt („nur beim Build“) und dann nicht mehr sauber erkennt, welche Pfade wann gesperrt sind.

Was die Quellen für die Praxis tragen – und wo die Grenzen bleiben

Die DocWiki zu System.TMonitor ist belastbar, wenn es um das Monitor-Verhalten geht: Enter/Exit als gegenseitiger Ausschluss, Wait als atomisches Freigeben und späteres Wiedererwerben des Locks, Pulse/PulseAll zum Wecken wartender Threads.[Quelle] Daraus folgt für den Betrieb: Wenn ein Cache intern mit Conditions arbeitet (z. B. „ein Thread baut, andere warten“), kann man das prinzipiell sauber ausdrücken.

Die Grenze: Aus dieser Dokumentation lässt sich in dieser Form keine harte Aussage ableiten, dass DCL ohne weitere Atomics/Memory-Barrier-Mechanik immer korrekt wäre. Genau deshalb ist der konservative Ansatz interessant, den Net-Base in einem Delphi-Beispiel für einen Typ-Cache zeigt: Zugriff aufs Dictionary wird durchgehend über TMonitor.Enter/Exit geschützt, also kein DCL, sondern „always lock“. Ergänzend wird Pre-Warming/Preload empfohlen, um Contention zu reduzieren, ohne das Korrektheitsrisiko zu erhöhen.[Quelle] Praktische Folgerung: Wer DCL einführt, um Locking zu vermeiden, sollte zuerst prüfen, ob Pre-Warming und saubere Lock-Kapselung denselben Nutzen liefern – mit deutlich weniger Randfall-Fallen.

Robuste Alternativen: drei Cache-Patterns, die sich gut betreiben lassen

Wenn der Cache im Alltag stabil laufen soll, zählt nicht nur Durchsatz, sondern auch Wartbarkeit: Wie schnell kann man in einem Incident verstehen, ob der Cache beteiligt ist? Wie klar ist das Lock-Konzept? Und wie leicht lässt sich das Verhalten testen?

Pattern Was es gut macht Typische Schwäche Wann es passt
Always-Lock mit System.TMonitor Einfaches Audit; jeder Dictionary-Zugriff geschützt; klare Failure-Domäne Skaliert begrenzt, wenn sehr viele parallele Reads auftreten Default für die meisten Caches in Services und Unternehmensanwendungen
Reader/Writer-Lock (z. B. TLightweightMREW) Viele parallele Reads; Writes exklusiv; weniger Contention im warmen Cache Komplexität steigt; Read→Write-Upgrade ist fehlerträchtig Read-lastige Caches mit nachweisbarer Contention
Pre-Warming / Preload Hot Path wird entlastet; Lastspitzen sinken; Verhalten wird planbarer Startup kostet Zeit; braucht Key-Wissen oder Zugriffsmuster Wenn typische Keys bekannt sind (Tabellen, Mandanten, Dokumenttypen)

Always-Lock: bewusst langweilig – und genau deshalb belastbar

Always-Lock heißt: Lesen und Schreiben des Dictionaries passiert ausschließlich innerhalb des Monitors. Das wirkt zunächst wie der Anti-Performance-Ansatz. In vielen Business-Workloads ist es aber der schnellste Weg zu einem System, das auch in sechs Monaten noch beherrschbar ist. Und: Wenn Contention auftritt, kann man sie messen und gezielt adressieren (Pre-Warming, Granularität, RW-Lock) – ohne die Grundkorrektheit zu riskieren.

Die erwähnte Cache-Implementierung im Net-Base-Beispiel geht genau diesen Weg: Ein gemeinsames Dictionary wird über TMonitor.Enter/Exit geschützt, statt mit DCL zu experimentieren.[Quelle]

Reader/Writer-Lock: für wirklich read-lastige Caches

Wenn ein Cache nach dem Warmup fast nur noch gelesen wird, kann ein Reader/Writer-Lock sinnvoll sein: Viele Threads dürfen parallel lesen, nur der Write-Pfad ist exklusiv. Damit reduziert man Blockaden, ohne den Container ungeschützt zu lassen.

Wichtig ist hier weniger die Klasse als die Disziplin: Der Code muss strikt trennen, wann gelesen und wann geschrieben wird. Halb-Optimierungen („erst read lock, dann doch write lock“) bringen schnell Komplexität, die später in Deadlocks oder schwer erklärbaren Wartezuständen endet.

Pre-Warming: Performance-Gewinn ohne Synchronisationsakrobatik

Pre-Warming verschiebt Arbeit bewusst aus dem Hot Path heraus. Statt dass der erste echte Request unter Last den Cache füllen muss, lädt man typische Keys gezielt vorher. Net-Base empfiehlt diese Richtung explizit als Ergänzung zu einem Monitor-geschützten Cache, um Contention zu reduzieren.[Quelle]

Wenn DCL wirklich auf den Tisch kommt: eine prüfbare Vorgehensweise

Manchmal gibt es den Punkt, an dem Always-Lock messbar nicht mehr reicht und ein RW-Lock allein nicht ausreicht oder nicht sauber passt. Dann wird DCL diskutiert. Spätestens dann sollte man es wie ein Risiko-Feature behandeln: mit klaren Regeln, Tests und messbaren Kriterien – nicht als „Micro-Optimierung“ in einem Nebencommit.

  1. Containerzugriff absichern: Stelle sicher, dass kein ungesperrter Zugriff auf ein mutierendes TDictionary stattfinden kann. Wenn Writes möglich sind, müssen Reads geschützt werden (Monitor/RW-Lock) oder die Datenstruktur/Phase muss so gestaltet sein, dass paralleles Lesen garantiert ungefährlich ist.
  2. Publikationspunkt definieren: Ein Value gilt erst als „veröffentlicht“, wenn er vollständig initialisiert ist. Keine Referenz in den Cache legen und danach noch weiter konfigurieren.
  3. Doppeltes Bauen bewerten: Prüfe, ob der Builder echte Seiteneffekte hat (Handles, Dateien, DB-Calls, Registrierungen). Wenn ja, ist „build twice“ kein harmloser Worst Case mehr, sondern ein Betriebsrisiko.
  4. Memory-Visibility nicht raten: Wenn du ohne Lock lesen willst, brauchst du eine belastbare Begründung, warum ein Thread nicht einen unfertigen Zustand sehen kann. Ohne explizite Grundlage wird DCL zur Hoffnung auf Implementierungsdetails.
  5. Reproduzierbare Lasttests: Baue einen Test, der denselben Key in vielen Threads wiederholt abfragt, inklusive künstlicher Delays im Builder. Ein einzelner erfolgreicher Lauf ist keine Evidenz.

Wenn sich Punkt 4 nicht sauber beantworten lässt, ist das Ergebnis der Prüfung oft simpel: DCL nicht als Standard einführen, sondern über Pre-Warming, RW-Lock oder Cache-Granularität lösen.

Debugging: Race-Conditions im Cache sichtbar machen, statt sie wegzusteppen

Breakpoints sind bei Race-Conditions häufig kontraproduktiv, weil sie das Timing verändern. Sinnvoller ist es, den Konflikt absichtlich zu provozieren und dabei so zu loggen, dass man die Timeline versteht.

Symptome, die zu DCL-/Cache-Races passen

  • Sporadische Access Violations in Dictionary-Operationen (TryGetValue, Add, ContainsKey) oder in Folgecode, der vermeintlich „garantiert initialisierte“ Werte nutzt.
  • Doppelte Initialisierung desselben Keys trotz Cache.
  • Deadlocks oder lange Hänger, die nur bei Parallelität auftreten (besonders bei verschachtelten Caches oder Lock-Ketten).
  • Fehler treten eher in Release als in Debug auf oder nur auf bestimmten Maschinen.

Telemetrie, die wirklich hilft

Logge für Cache-Zugriffe: Thread-ID, Key, Phase (First-Check, Lock-Enter, Second-Check, Build-Start, Build-Ende, Publish) und Dauer. Ergänze bei Monitor-Nutzung optional TryEnter-basierte Messungen, um Wartezeiten zu sehen. Das Ziel ist nicht „viel Log“, sondern ein klarer Ablauf, der bei einem Incident zeigt, ob Containerzugriffe außerhalb des Schutzes passieren oder ob Builds parallel laufen.

Performance-Realität: Warum TMonitor „plötzlich teuer“ wirken kann

Ein häufiger Auslöser für DCL ist echter Performance-Schmerz: Lock-Contention frisst Durchsatz. Dabei ist nicht jedes Problem ein Argument für DCL, sondern oft ein Hinweis auf Skalierungsdetails.

Es gibt Analysen, die bei TMonitor auf potenzielle Performance-Effekte unter hoher Parallelität hinweisen, etwa durch Cache-Line-Contention/False Sharing in internen Strukturen.[Quelle] Das ist kein Korrektheitsargument gegen Monitore, aber ein guter Reminder: Wer optimieren will, sollte erst messen und dann das passende Mittel wählen.

Typische, oft risikoärmere Hebel vor DCL:

  • Pre-Warming, damit der Builder nicht unter Last läuft.
  • Cache-Granularität ändern (weniger Keys, weniger Writes, weniger Resizes).
  • Write-Pfad kurz halten (Build-Arbeit nicht im Lock, aber Containerzugriff weiterhin geschützt).
  • RW-Lock statt Monitor, wenn Reads dominieren.

Wann sich der Aufwand für einen thread-sicheren Cache lohnt – und wann nicht

Ein generischer Cache ist Infrastruktur. Er lohnt sich, wenn er wiederholt teure Arbeit ersetzt (RTTI-Scanning, komplexe Mapper, Parser, Konfigurationen) und wenn echte Parallelität existiert (Service, Job-Worker, Task-basierte Verarbeitung, mehrere gleichzeitige Requests).

Er ist oft Overhead, wenn:

  • die betroffenen Codepfade praktisch nur im UI-Thread laufen,
  • die Werte billig sind und kaum messbaren Benefit bringen,
  • der Cache sicherheits- oder abrechnungsrelevante Logik indirekt beeinflusst, aber die Optimierung nur spekulativ ist.

Der Punkt, an dem es „sauber“ wird

Double-Checked Locking sieht im Review häufig nach „Profi-Optimierung“ aus. In Delphi wird es bei generischen Caches aber schnell zu einem Mix aus Container-Risiko, Sichtbarkeitsfragen und schwer reproduzierbaren Fehlerbildern. Der saubere Weg ist meistens: erst Korrektheit und Beweisbarkeit herstellen (Always-Lock oder Reader/Writer), dann Pre-Warming und Messung einsetzen, und erst ganz am Ende – wenn überhaupt – DCL für eng begrenzte Fälle diskutieren.

Wenn ihr einen bestehenden Cache stabilisieren oder eine Modernisierung so planen wollt, dass Parallelität im Betrieb nicht zur Lotterie wird, lohnt sich eine kurze technische Einordnung anhand eurer konkreten Zugriffsmuster und Lastprofile. Kontakt aufnehmen.

Quellen und weiterfuehrende Informationen

Die fachlichen Kernaussagen wurden anhand der folgenden externen Quellen redaktionell eingeordnet.

  1. DoubleCheckedLocking (errorprone.info)
    Double-Checked Locking ist ohne garantierte Speicher-Schranken/Visibility fehlerträchtig; Reordering kann eine Referenz vor vollständiger Initialisierung sichtbar machen.
  2. System.TMonitor – RAD Studio API Documentation (docwiki.embarcadero.com)
    System.TMonitor bietet Enter/Exit/TryEnter sowie Wait/Pulse/PulseAll mit definiertem Monitor-Verhalten, ohne explizite DCL-Publish-Zusage in der Dokumentation.
  3. Vcl.Forms.TMonitor – RAD Studio API Documentation (docwiki.embarcadero.com)
    Vcl.Forms.TMonitor beschreibt einen Bildschirm-Monitor und ist nicht der Threading-Monitor (Begriffsverwechslung).
  4. Delphi RTTI for mapping without magic | Net-Base (net-base-software-gmbh.de)
    Beispiel für einen Delphi-Cache: Zugriff wird per TMonitor.Enter/Exit geschützt; zusätzlich wird Pre-Warming empfohlen, um Contention zu reduzieren.
  5. A Fistful of TMonitors – DelphiTools (www.delphitools.info)
    Analyse zu TMonitor: mögliche Performance-/Contention-Effekte (z. B. False Sharing) können Locking unter Last beeinflussen.

Für dieses Thema sind auch Delphi Tmonitor und Double-Checked Locking Delphi wichtig. Der Beitrag ordnet diese Aspekte verständlich ein und zeigt, worauf es im Alltag ankommt.

Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

Passo successivo

Quando un tema diventa un progetto reale, architettura, sistemi esistenti e gestione operativa dovrebbero essere considerati insieme fin dalle fasi iniziali.

Non forniamo solo supporto per questioni isolate, ma anche quando da frammenti di codice sorgente, tematiche legacy o idee di portale deve nascere un progetto aziendale solido.

  • Stato attuale, stato obiettivo e rischi tecnici vengono valutati insieme.
  • REST, l'accesso ai dati, i portali e il rollout non vengono rinviati a fasi successive.
  • Vede in anticipo quale percorso è economicamente e operativamente sostenibile.

Condividi il post

Condividi direttamente questo articolo

LinkedIn, X, XING, Facebook, WhatsApp e e-mail sono immediatamente disponibili. Per Instagram prepariamo direttamente il link e un breve testo.

E-mail

Instagram si apre in una nuova scheda. Il link e il breve testo vengono copiati prima negli appunti.

Scrivi un commento

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert