Wie ein Intertangle entsteht
Zwei Subsysteme beginnen als unabhängige Module. Mit der Zeit akkumuliert jedes ein Feld auf einem gemeinsamen God-Object: eine globale Konfigurationsstruktur, ein Singleton-Manager, eine statische Klasse. Jede Ergänzung ist für sich genommen korrekt. Die Kopplung bleibt bei Tests in kleinem Maßstab unsichtbar.
Drei Substrate, in denen dies verkalkt ist:
VLC Media Player. Audio, Video und Playlist teilen sich eine einzige Sperre, die einen globalen Player-Zustand schützt. Eine Anfrage zum Überspringen zu einem Zeitstempel erwirbt die Sperre, ändert die Wiedergabeposition und leert den Audio-Puffer. Das Video-Subsystem, das auf dieselbe Sperre wartet, blockiert. Das Playlist-Subsystem, das ebenfalls wartet, kann nicht vorab laden. Ergebnis: drei unabhängige Subsysteme werden über ein einziges Zustandsobjekt serialisiert. Leistungskosten: O(N) Lock-Contention, wobei N = Anzahl der Subsysteme, alle proportional zur Operationslatenz.
Redis Event Loop. AOF-Fsync (Festplattenschreibvorgang), Replikation (Netzwerk-Schreibvorgang) und Befehlsausführung (CPU) teilen sich die single-threaded Event Loop. Jede Operation ist für sich genommen korrekt. Ein langsamer Fsync blockiert die Befehlsausführung. Replikationsverzögerungen verstärken sich unter Schreiblast. Der Kopplungspunkt: ein einziger Ausführungskontext, der Operationen mit unterschiedlichen Latenzprofilen gemeinsam nutzt.
LevelDB VersionSet. Schreibpfad (Memtable-Flush) und Hintergrund-Compaction teilen sich die VersionSet-Sperre. Ein Compaction-Job hält die Sperre für Dutzende Millisekunden. Der Schreibpfad wird blockiert. Beide Operationen sind notwendig. Die Kopplung ist strukturell, nicht zeitlich bedingt.
Die entscheidende Unterscheidung
Ein Intertangle weist eine strukturelle Kopplung auf, kein Timing-Problem. Eine Race Condition liegt vor, wenn zwei Threads ohne Synchronisation auf gemeinsam genutzten Zustand zugreifen. Die Lösung: Hinzufügen eines Mutex.
Ein Intertangle: Zwei Subsysteme teilen sich Zustand konstruktionsbedingt. Das Hinzufügen eines Mutex behebt die Kopplung nicht, sondern serialisiert lediglich den Zugriff. Die Subsysteme teilen weiterhin Zustand. Der Engpass verschärft sich.
Das Hinzufügen eines Mutex zu einem VLC-Intertangle verschlimmert das Problem: Audio, Video und Playlist warten nun alle auf eine einzige Sperre. Die strukturelle Lösung: Jedes Subsystem erhält seinen eigenen Zustand. Phasen-Snapshot: Einfrieren eines Snapshots des gemeinsam genutzten Zustands an der Phasengrenze, unabhängiges Lesen durch jedes Subsystem und Zurückschreiben der Änderungen am Ende.
Strukturell vs. Zeitlich
Die zentrale diagnostische Frage bei einem Intertangle: Würde das Hinzufügen eines Mutex das Problem beheben oder verschlimmern?
Eine Race Condition: Hinzufügen eines Mutex behebt sie. Korrekte Zugriffsreihenfolge verhindert die Korruption.
Ein Intertangle: Hinzufügen eines Mutex serialisiert den Zugriff, erhält aber die strukturelle Kopplung. Die Subsysteme teilen weiterhin Zustand. Unter Last blockieren sie sich weiterhin gegenseitig. Der Engpass wird enger.
Wie man einen Intertangle findet
Drei Erkennungssignale:
1. Gemeinsam genutzte veränderbare Felder zwischen Subsystemen. Ein God-Object mit Feldern, die von mehr als einem Subsystem gelesen und geschrieben werden. Wenn das Entfernen des Feldzugriffs eines Subsystems ein anderes Subsystem beschädigt, teilen sie sich Zustand.
2. Ein einzelnes Mutex, das unzusammenhängende Operationen schützt. Ein Lock, der Audio-Flush UND Video-Decode UND Playlist-Fetch schützt: drei Subsysteme mit unterschiedlichen Latenzprofilen, die alle aufeinander warten. Der Geruch: unzusammenhängende Operationen unter demselben Lock-Namen.
3. Performance-Regressions bei zusätzlicher Last. Die Latenz von Operation A steigt, wenn Operation B gleichzeitig ausgeführt wird, obwohl A & B unabhängig erscheinen. Sie sind nicht unabhängig: sie teilen sich Zustand.
Die Phase-Snapshot-Lösung
Phase-Snapshot-Pattern:
# VORHER: Subsysteme lesen und schreiben direkt gemeinsam genutzten Zustand
class GameWorld:
position = {} # gemeinsamer veränderlicher Zustand
velocity = {} # gemeinsamer veränderlicher Zustand
def physics_tick(world):
for entity in world.entities:
world.position[entity] += world.velocity[entity] # schreibt gemeinsamen Zustand mitten in der Schleife
# NACH: Snapshot vor der Phase eingefroren; Schreibvorgänge gehen in den next_state-Puffer
def physics_tick(world):
snapshot = world.freeze() # unveränderbare Ansicht
next_state = {}
for entity in snapshot.entities:
next_state[entity] = snapshot.position[entity] + snapshot.velocity[entity]
world.merge(next_state) # atomarer Merge an der Phasengrenze
Jedes Subsystem liest den Snapshot. Kein Subsystem schreibt hinein. Schreibvorgänge werden in einem Puffer gesammelt und am Phasenübergang atomar zusammengeführt. Die Subsysteme laufen nun unabhängig: keine Lock-Kontention, keine Reihenfolgeabhängigkeit, keine versteckte Kopplung.
Den Fix anwenden
Ein Team meldet einen Fehler: Im Spiel-Engine schreiben sowohl das Animationssystem als auch das Kollisionssystem in dasselbe Entity-Transform-Objekt. Wenn beide im selben Tick laufen, hängen die Kollisionsergebnisse davon ab, ob die Animation zuerst ausgeführt wurde. Ein Mutex hat die Reihenfolge sichergestellt, aber jetzt blockiert die Animation, sobald die Kollision eine Broad-Phase-Sweep durchführt.