Hoe een Intertangle ontstaat
Twee subsystemen beginnen als onafhankelijke modules. Na verloop van tijd krijgt elk een veld op een gedeeld god-object: een globale config-struct, een singleton-manager of een statische klasse. Elke toevoeging is op zichzelf correct. De koppeling blijft onzichtbaar bij kleinschalige tests.
Drie situaties waarin dit patroon verhardt:
VLC media player. Audio, video en afspeellijst delen één lock die de globale player-state bewaakt. Een verzoek om naar een tijdstip te springen verkrijgt de lock, wijzigt de afspeelpositie en leegt de audio-buffer. Het videosubsysteem, wachtend op dezelfde lock, blokkeert. Het afspeellijst-subsysteem, eveneens wachtend, kan geen prefetch uitvoeren. Resultaat: drie onafhankelijke subsystemen worden geserialiseerd via één state-object. Prestatiekosten: O(N) lock-contention waarbij N = aantal subsystemen, allemaal evenredig met de latentie van de operatie.
Redis event loop. AOF fsync (schijf-schrijven), replicatie (netwerk-schrijven) en commando-uitvoering (CPU) delen de single-threaded event loop. Elk afzonderlijk: correct. Een langzame fsync blokkeert commando-uitvoering. Replicatievertraging neemt toe onder schrijfdruk. Het koppelpunt: één uitvoeringcontext gedeeld door operaties met verschillende latentieprofielen.
LevelDB VersionSet. Schrijfpad (memtable-flush) en achtergrondcompactie delen de VersionSet-vergrendeling. Een compactietaak houdt de lock tientallen milliseconden vast. Schrijfpad blokkeert. Beide operaties: noodzakelijk. De koppeling: structureel, niet timing-gerelateerd.
Het kritieke onderscheid
Een Intertangle heeft een structurele koppeling, geen timingprobleem. Een race condition: twee threads benaderen gedeelde toestand zonder synchronisatie. Oplossing: voeg een mutex toe.
Een Intertangle: twee subsystemen delen toestand per ontwerp. Een mutex toevoegen lost de koppeling niet op; het serialiseert de toegang. De subsystemen delen nog steeds toestand. De bottleneck wordt strakker.
Een mutex toevoegen aan een VLC Intertangle maakt het erger: audio, video en afspeellijst wachten nu allemaal op één lock. De structurele oplossing: geef elk subsysteem zijn eigen toestand. Fase-snapshot: bevries een snapshot van de gedeelde toestand op de fasegrens, laat elk subsysteem de snapshot onafhankelijk lezen en schrijf wijzigingen terug aan het einde.
Structureel vs Timing
De belangrijkste diagnostische vraag voor een Intertangle: zou het toevoegen van een mutex het oplossen, of zou het het erger maken?
Een race condition: het toevoegen van een mutex lost het op. Correcte toegangvolgorde voorkomt de corruptie.
Een Intertangle: het toevoegen van een mutex serialiseert de toegang maar behoudt de structurele koppeling. De subsystemen delen nog steeds state. Onder belasting blokkeren ze elkaar nog steeds. De bottleneck wordt smaller.
Hoe vind je een Intertangle
Drie detectiesignalen:
1. Gedeelde muteerbare velden tussen subsystemen. Een god-object met velden die door meer dan één subsysteem worden gelezen en geschreven. Als het verwijderen van de veldtoegang van één subsysteem een ander subsysteem breekt, delen ze state.
2. Eén mutex die ongerelateerde operaties bewaakt. Eén lock die audio-flush EN video-decode EN playlist-fetch bewaakt: drie subsystemen met verschillende latentieprofielen, die allemaal op elkaar wachten. De geur: ongerelateerde operaties onder dezelfde lock-naam.
3. Prestatie-regressie bij toenemende belasting. De latentie van operatie A stijgt wanneer operatie B tegelijkertijd draait, ook al lijken A & B onafhankelijk. Ze zijn niet onafhankelijk: ze delen state.
De Phase-Snapshot Fix
Phase-snapshot-patroon:
# VOOR: subsystemen lezen en schrijven direct naar gedeelde state
class GameWorld:
position = {} # gedeelde muteerbare staat
velocity = {} # gedeelde muteerbare staat
def physics_tick(world):
for entity in world.entities:
world.position[entity] += world.velocity[entity] # schrijft gedeelde staat midden in de lus
# NA: snapshot bevroren vóór fase; schrijfacties gaan naar next_state-buffer
def physics_tick(world):
snapshot = world.freeze() # onveranderbare weergave
next_state = {}
for entity in snapshot.entities:
next_state[entity] = snapshot.position[entity] + snapshot.velocity[entity]
world.merge(next_state) # atomische merge op fasegrens
Elk subsysteem leest de snapshot. Geen subsysteem schrijft ernaar. Schrijfacties accumuleren in een buffer en worden atomair samengevoegd op de fasegrens. Subsystemen kunnen nu onafhankelijk uitvoeren: geen lock-contention, geen ordeningsafhankelijkheid, geen verborgen koppeling.
Pas de Fix toe
Een team meldt een defect: het animatiesysteem en het collisionsysteem van hun game-engine schrijven beide naar een gedeeld entity-transform-object. Wanneer beide in dezelfde tick draaien, hangen de collision-resultaten af van of animatie eerst heeft gerund. Het toevoegen van een mutex loste de ordening op, maar nu stopt animatie telkens wanneer collision een broad-phase sweep uitvoert.