Comment un Intertangle se forme
Deux sous-systèmes commencent leur vie en tant que modules indépendants. Au fil du temps, chacun accumule un champ sur un god-object partagé : une structure de configuration globale, un gestionnaire singleton, une classe statique. Chaque ajout est correct isolément. Le couplage reste invisible lors des tests à petite échelle.
Trois substrats où cela s’est calcifié :
VLC media player. Audio, vidéo et playlist partagent un verrou unique protégeant un état global du lecteur. Une requête de saut à un horodatage acquiert le verrou, modifie la position de lecture et vide le tampon audio. Le sous-système vidéo, en attente du même verrou, se bloque. Le sous-système playlist, lui aussi en attente, ne peut pas précharger. Résultat : trois sous-systèmes indépendants sérialisés via un seul objet d’état. Coût de performance : contention de verrou O(N) où N = nombre de sous-systèmes, proportionnel à la latence de l’opération.
Boucle d’événements Redis. Le fsync AOF (écriture disque), la réplication (écriture réseau) et l’exécution des commandes (CPU) partagent la boucle d’événements mono-thread. Chacun est correct isolément. Un fsync lent bloque l’exécution des commandes. Le lag de réplication s’aggrave sous forte charge d’écriture. Le point de couplage : un contexte d’exécution unique partagé par des opérations ayant des profils de latence différents.
VersionSet de LevelDB. Le chemin d’écriture (flush de la memtable) et la compaction en arrière-plan partagent le verrou du VersionSet. Une tâche de compaction conserve le verrou pendant des dizaines de millisecondes. Le chemin d’écriture est bloqué. Les deux opérations sont nécessaires. Le couplage est structurel, pas temporel.
La distinction critique
Un Intertangle présente un couplage structurel, pas un problème de synchronisation. Une condition de course : deux threads accèdent à un état partagé sans synchronisation. Solution : ajouter un mutex.
Un Intertangle : deux sous-systèmes partagent un état par conception. Ajouter un mutex ne résout pas le couplage ; il sérialise l’accès. Les sous-systèmes continuent de partager l’état. Le goulot d’étranglement se resserre.
Ajouter un mutex à un Intertangle VLC empire la situation : audio, vidéo et playlist attendent désormais un seul verrou. La correction structurelle : donner à chaque sous-système son propre état. Snapshot de phase : figer un instantané de l’état partagé à la frontière de phase, permettre à chaque sous-système de lire l’instantané indépendamment, fusionner les écritures à la fin.
Structurel vs Temporel
La question diagnostique clé pour un Intertangle : ajouter un mutex le résoudrait-il, ou l’aggraverait-il ?
Une condition de course : l’ajout d’un mutex la corrige. Un ordre d’accès correct élimine la corruption.
Un intertangle : l’ajout d’un mutex sérialise l’accès mais conserve le couplage structurel. Les sous-systèmes partagent toujours l’état. Sous charge, ils se bloquent toujours mutuellement. Le goulot d’étranglement se resserre.
Comment repérer un intertangle
Trois signaux de détection :
1. Champs mutables partagés entre sous-systèmes. Un god-object avec des champs lus et écrits par plus d’un sous-système. Si la suppression de l’accès à un champ d’un sous-système casse un autre sous-système, ils partagent un état.
2. Un seul mutex protégeant des opérations non liées. Un verrou protégeant à la fois le flush audio ET le décodage vidéo ET la récupération de playlist : trois sous-systèmes avec des profils de latence différents, tous en attente les uns des autres. L’odeur : des opérations non liées sous le même nom de verrou.
3. Régression de performance lors de l’ajout de charge. La latence de l’opération A augmente lorsque l’opération B s’exécute simultanément, même si A et B semblent indépendants. Ils ne sont pas indépendants : ils partagent un état.
La correction Phase-Snapshot
Modèle de phase snapshot :
# AVANT : les sous-systèmes lisent et écrivent directement l’état partagé
class GameWorld:
position = {} # état mutable partagé
velocity = {} # état mutable partagé
def physics_tick(world):
for entity in world.entities:
world.position[entity] += world.velocity[entity] # écriture de l'état partagé pendant la boucle
# APRÈS : instantané figé avant la phase ; les écritures vont dans le tampon next_state
def physics_tick(world):
snapshot = world.freeze() # vue immuable
next_state = {}
for entity in snapshot.entities:
next_state[entity] = snapshot.position[entity] + snapshot.velocity[entity]
world.merge(next_state) # fusion atomique à la frontière de phase
Chaque sous-système lit l'instantané. Aucun sous-système n'y écrit. Les écritures s'accumulent dans un tampon et fusionnent atomiquement à la frontière de phase. Les sous-systèmes s'exécutent désormais indépendamment : aucune contention de verrou, aucune dépendance d'ordre, aucun couplage caché.
Appliquer la correction
Une équipe signale un défaut : le système d'animation et le système de collision du moteur de jeu écrivent tous deux dans un objet de transformation d'entité partagé. Lorsque les deux s'exécutent dans le même tick, les résultats de collision dépendent de l'exécution préalable de l'animation. L'ajout d'un mutex a corrigé l'ordre, mais l'animation se bloque désormais lorsque la collision effectue un balayage large.