Begriff
Race Condition
Warum wichtig?
Dieser Begriff ist ein Knoten im SengakujiWorks-Wissensnetz. Nutze Level 0 für die erste Einordnung, Level 1 für Praxis, Level 2 für technische Struktur und Level 3 für Grenzen, Fallstricke und Expertenkontext.
Stell dir ein gemeinsames Bankkonto vor (100 € Guthaben). Du und deine Frau heben genau gleichzeitig (in der gleichen Millisekunde) 100 € am Automaten ab. Automat A prüft: "100 € da? Ja." (Gibt Geld). Automat B prüft: "100 € da? Ja." (Gibt Geld). Ergebnis: Ihr habt 200 € Bargeld, aber nur 100 € auf dem Konto. Die Bank hat 100 € Verlust gemacht. Das ist eine Race Condition: Ein Fehler, der passiert, weil zwei Prozesse "um die Wette rennen" und das Timing nicht stimmt.
Merksatz: Ein Fehler in Systemen, bei dem das Ergebnis von der zufälligen zeitlichen Reihenfolge von Ereignissen abhängt.
Gefährlich, weil sie nicht determinitisch sind. Manchmal passieren sie (wenn der Computer gerade ungünstig getaktet ist), manchmal wochenlang nicht. Man nennt sie "Heisenbugs" (weil sie verschwinden, wenn man genau hinschaut/debuggt). Lösung: Locks (Sperren) oder Atomare Operationen. "Während ich abhebe, darf niemand anders auf das Konto zugreifen."
1. Read-Modify-Write
Das klassische Muster für Race Conditions:
- Read: Lese Wert (x = 10).
- Modify: Erhöhe (x = 11).
- Write: Schreibe zurück (x = 11). Wenn Thread B dazwischengrätscht (z. B. zwischen 1 und 2), rechnet er noch mit dem alten Wert. Beide schreiben am Ende "11", obwohl es "12" sein müsste (Lost Update).
2. Check-Time vs. Use-Time (TOCTOU)
Security-Problem. "Darf User X die Datei löschen?" -> Ja (Check). ... (Hacker tauscht Datei schnell gegen Systemdatei aus) ... "Lösche Datei!" -> (Use). Boom, Systemdatei gelöscht. Man muss prüfen und handeln in einem einzigen, ununterbrechbaren Schritt.
1. Data Races vs. Race Conditions
Obwohl oft synonym genutzt, definiert die Fachliteratur (wie C++) hier streng um.
Ein Data Race ist ein extrem tiefgehendes Hardware-Speicherfehler-Modell: Wenn mindestens zwei Hardware-Threads auf exakt dieselbe Speicheradresse (Memory Location) im C-Pointer zugreifen, und mindestens einer schreibt, und beide tun es unstrukturiert (ohne Synchronisations-Primitiven), ist das Resultat laut LLVM "Undefined Behavior" (Der Compiler darf die CPU theoretisch brennen lassen).
Eine Race Condition ist ein Logik-Fehler. Du kannst perfekten, Race-freien Mutex Code schreiben (kein Data Race), aber trotzdem eine transaktionale Race Condition im System haben (Z. B. Thread 1 sperrt das Geld, liest 100$, entsperrt. Thread 2 zieht 100$ ab. Thread 1 bucht 20$ zu, speichert 120$. Das Locking war perfekt, das Ergebnis ist falsch).
2. Time-of-Check to Time-of-Use (TOCTOU) Exploits
Security-Kritische Race Actions existieren oft bei IO-Aufrufen (Filesysteme oder Kernel).
Stell dir den Unix Command passwd vor. Die Binary läuft unter Root.
Der Code prüft: if (access("/temp/myfile", W_OK)) (Check: darf User hier posten?).
Zeit vergeht (Kontext Switch der CPU -> Hacker-Skript).
Das Hacker-Skript löscht myfile blitzschnell und erstellt per Symlink ein C-Link namens myfile, das auf /etc/passwd gerichtet ist!
Der root-Code fährt fort: file = open("/temp/myfile") (Use).
Der Code überschreibt die Passwortdatei der Server Matrix mit Malware-Daten! In Linux wurde das TOCTOU oft mit Syscalls wie openat() oder O_EXCL in Verbindung mit Flag-Directory-Locks ausgemerzt.
3. Memory Barriers (Fences) & Out-of-Order Execution
Auf höchster Skalierungsebene sind Mutexe / Locks ruinös für die Latenz (Pessimistic Locking). Entwickler (wie in hochperformanten Java Ring-Buffern wie LMAX Disruptor) migrieren auf lock-free "Wait-Free" Algorithmen.
Hier nutzt man direkte "Atomic Operations" (z. B. Compare-and-Swap CPU Instruktionen auf L1-Cache Level).
Das brutale Gotcha: Moderne Intel/AMD CPUs führen deinen Code oft nicht in der Reihenfolge aus, wie er im Assembler steht! Sie reordnen (Out-Of-Order Execution) Variablen für Speed.
Wenn Thread 1 die Variable $X=1$ und $Flag=True$ setzt, kann Thread 2 durchaus $Flag=True$ lesen, aber für $X$ immer noch den alten Wert sehen! Um Data-Races hierbei tief in der x86-Branch Prediction zu vermeiden, muss man drastische Memory Fences in den Compiler jagen, welche der CPU hart befehlen: Jede Register-Zuweisung MUSS in den geteilten L3 Cache versickert sein, bevor der Takt der Zeile 2 triggert.
Quick-Check
Sind Single-Core-CPUs sicher?
Nein. Auch dort gibt es "Preemptive Multitasking". Das Betriebssystem kann Thread A mitten in der Arbeit unterbrechen und Thread B starten.Wie verhindere ich das?
Mit Mutex (Mutual Exclusion). Ein "Stick", den nur einer halten darf. Wer den Stick hat, darf schreiben. Alle anderen müssen warten.Ist JavaScript sicher?
Meistens ja, weil JS (in Node.js und Browser) Single-Threaded ist (Event Loop). Es führt Code immer am Stück aus. Ausnahmen: Worker Threads oder Datenbank-Zugriffe.