Race Condition

Fehler, bei dem das Ergebnis eines Programms von der zeitlichen Reihenfolge mehrerer Threads abhängt, die gleichzeitig auf dieselbe Ressource zugreifen.

Auf dieser Seite

Race Condition (deutsch: Wettlaufsituation) ist ein Fehler in der Softwareentwicklung, der auftritt, wenn das Ergebnis eines Programms von der zeitlichen Reihenfolge abhängt, in der mehrere Threads oder Prozesse ausgeführt werden. Wie bei einem echten Rennen “wetteifern” mehrere Ausführungsstränge darum, wer zuerst auf eine gemeinsam genutzte Ressource zugreift, mit unvorhersehbaren Ergebnissen.

Race Conditions gehören zu den tückischsten Fehlern in der Programmierung: Sie treten oft nur sporadisch auf und sind schwer reproduzierbar. Das macht die Fehlersuche besonders schwierig. Während das Programm in den meisten Fällen korrekt funktioniert, kann es unter bestimmten zeitlichen Umständen zu Datenkorruption, Abstürzen oder falschen Ergebnissen kommen.

Wie entsteht eine Race Condition?

Eine Race Condition entsteht, wenn mehrere Threads oder Prozesse gleichzeitig auf eine gemeinsam genutzte Ressource zugreifen, etwa eine Variable, eine Datei oder einen Datenbankeintrag. Das Problem liegt darin, dass moderne Betriebssysteme die Ausführungsreihenfolge von Threads nicht garantieren. Der Scheduler kann einen Thread jederzeit unterbrechen und einen anderen ausführen.

Stell dir vor, zwei Threads wollen gleichzeitig einen Zähler um 1 erhöhen. Was wie eine einfache Operation aussieht (counter++), besteht intern aus drei Schritten: Wert lesen, Wert erhöhen, Wert zurückschreiben. Wenn Thread A den Wert liest (z.B. 5), dann Thread B ebenfalls den Wert 5 liest, beide erhöhen und zurückschreiben, ist das Endergebnis 6 statt 7. Ein Inkrement ging verloren.

Das Bankkonto-Beispiel

Ein klassisches Beispiel ist ein Bankkonto mit zwei Inhabern. Angenommen, der Kontostand beträgt 1000 Euro. Person A will 800 Euro abheben, Person B gleichzeitig 500 Euro. Ohne korrekte Synchronisation könnte folgendes passieren:

  1. Person A liest Kontostand: 1000 Euro
  2. Person B liest Kontostand: 1000 Euro
  3. Person A prüft: 800 < 1000 - OK, Abhebung erlaubt
  4. Person B prüft: 500 < 1000 - OK, Abhebung erlaubt
  5. Person A hebt ab: 1000 - 800 = 200 Euro
  6. Person B hebt ab: 1000 - 500 = 500 Euro

Je nachdem, wer zuletzt schreibt, steht der Kontostand auf 200 oder 500 Euro, obwohl insgesamt 1300 Euro abgehoben wurden. Die Bank hat Geld verloren, weil die Prüfung und die Buchung nicht als unteilbare (atomare) Operation ausgeführt wurden.

Arten von Race Conditions

Race Conditions lassen sich in verschiedene Kategorien einteilen, je nachdem, welches Muster zu dem Problem führt. Das Verständnis dieser Muster hilft dabei, potenzielle Race Conditions im eigenen Code zu erkennen.

Read-Modify-Write (Lesen-Ändern-Schreiben)

Dies ist das häufigste Muster: Ein Thread liest einen Wert, verändert ihn und schreibt ihn zurück. Zwischen dem Lesen und Schreiben kann ein anderer Thread den Wert bereits verändert haben. Das Zähler-Beispiel von oben (counter++) gehört in diese Kategorie.

// Problematisch: Read-Modify-Write ohne Synchronisation
public class Counter {
    private int count = 0;
    
    public void increment() {
        count++;  // NICHT thread-sicher!
    }
}

Check-Then-Act (Prüfen-Dann-Handeln)

Bei diesem Muster wird zuerst eine Bedingung geprüft und dann basierend auf dem Ergebnis eine Aktion ausgeführt. Das Problem: Zwischen Prüfung und Aktion kann sich die Bedingung bereits geändert haben. Das Bankkonto-Beispiel ist ein typischer Fall von Check-Then-Act.

// Problematisch: Check-Then-Act ohne Synchronisation
public boolean abheben(int betrag) {
    if (kontostand >= betrag) {     // Check
        kontostand -= betrag;        // Act
        return true;
    }
    return false;
}

Time-of-Check to Time-of-Use (TOCTOU)

TOCTOU ist eine spezielle Form von Check-Then-Act, die häufig bei Dateizugriffen auftritt. Ein Programm prüft beispielsweise, ob eine Datei existiert, und öffnet sie dann. In der Zwischenzeit könnte ein anderer Prozess die Datei gelöscht oder ersetzt haben. TOCTOU-Schwachstellen sind ein bekanntes Sicherheitsproblem, das Angreifer ausnutzen können.

Wie vermeidet man Race Conditions?

Es gibt verschiedene Techniken, um Race Conditions zu verhindern. Die Wahl der richtigen Methode hängt vom konkreten Anwendungsfall und den Anforderungen an Performance ab.

Locks und Mutexe

Ein Lock (Sperre) oder Mutex (Mutual Exclusion) stellt sicher, dass immer nur ein Thread einen kritischen Codeabschnitt betreten kann. Andere Threads müssen warten, bis der Lock freigegeben wird. In Java kannst du das synchronized-Schlüsselwort verwenden, in C# das lock-Statement.

// Thread-sicher durch synchronized
public class Counter {
    private int count = 0;
    
    public synchronized void increment() {
        count++;  // Jetzt thread-sicher
    }
}

Atomare Operationen

Atomare Operationen sind Operationen, die von der Hardware als unteilbar garantiert werden. Die CPU führt sie in einem Schritt aus, sodass kein anderer Thread dazwischenkommen kann. Programmiersprachen bieten spezielle atomare Datentypen wie AtomicInteger in Java oder Interlocked-Methoden in C#.

// Thread-sicher durch atomaren Datentyp
import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet();  // Atomare Operation
    }
}

Semaphoren

Ein Semaphor ist ein Synchronisationsmechanismus, der eine bestimmte Anzahl von Threads gleichzeitig in einen kritischen Bereich lässt. Im Gegensatz zu einem Mutex, der nur einen Thread erlaubt, kann ein Semaphor mehrere Zugriffe koordinieren, etwa wenn maximal 5 Threads gleichzeitig auf eine Ressource zugreifen dürfen.

Immutability

Eine elegante Strategie ist die Verwendung von unveränderlichen (immutable) Datenstrukturen. Wenn ein Objekt nach seiner Erstellung nicht mehr verändert werden kann, können auch keine Race Conditions entstehen. Jede Änderung erzeugt ein neues Objekt. Funktionale Programmiersprachen nutzen dieses Prinzip intensiv.

Race Conditions vs. Deadlock

Race Conditions werden oft mit Deadlocks verwechselt, obwohl es sich um unterschiedliche Probleme handelt. Bei einer Race Condition führen Threads ihren Code aus, nur in einer unerwarteten Reihenfolge. Bei einem Deadlock blockieren sich zwei oder mehr Threads gegenseitig, sodass keiner mehr fortfahren kann.

AspektRace ConditionDeadlock
ProblemFalsche/unvorhersehbare ErgebnisseProgramm hängt, keine Ausführung
UrsacheFehlende SynchronisationZu viel/falsche Synchronisation
ErkennbarkeitSporadisch, schwer reproduzierbarOffensichtlich (Programm reagiert nicht)
LösungLocks hinzufügen, atomare OperationenLock-Reihenfolge beachten, Timeouts

Ironischerweise kann der Versuch, Race Conditions mit Locks zu beheben, zu Deadlocks führen, wenn mehrere Locks in unterschiedlicher Reihenfolge angefordert werden. Die Kunst liegt darin, die richtige Balance zwischen ausreichender Synchronisation und Deadlock-Vermeidung zu finden.

Praxisbeispiel: Singleton-Pattern

Das Singleton-Pattern ist ein klassisches Beispiel, bei dem Race Conditions auftreten können. Ein Singleton soll sicherstellen, dass nur eine einzige Instanz einer Klasse existiert. Die naive Implementierung ist jedoch nicht thread-sicher:

// Problematisch: Race Condition beim Singleton
public class Singleton {
    private static Singleton instance;
    
    public static Singleton getInstance() {
        if (instance == null) {          // Thread A prüft: null
            instance = new Singleton();   // Thread B prüft auch: null
        }                                 // Beide erzeugen Instanzen!
        return instance;
    }
}

Die thread-sichere Lösung verwendet entweder synchronized, das Double-Checked Locking Pattern mit volatile, oder noch besser: die Initialisierung durch den Classloader oder ein Enum.

// Thread-sicher: Initialisierung durch Classloader
public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

Erkennung und Debugging

Race Conditions zu finden ist schwierig, weil sie nur unter bestimmten zeitlichen Bedingungen auftreten. Es gibt jedoch Tools und Techniken, die dabei helfen:

  • Thread Sanitizer (TSan): Ein Tool für C/C++, das Race Conditions zur Laufzeit erkennt
  • Static Analysis Tools: Werkzeuge wie FindBugs (Java) oder Coverity analysieren den Code auf potenzielle Probleme
  • Code Reviews: Erfahrene Entwickler können typische Muster erkennen
  • Stress-Tests: Viele gleichzeitige Threads erhöhen die Wahrscheinlichkeit, Race Conditions zu triggern
  • Logging mit Zeitstempeln: Hilft bei der Rekonstruktion der Ausführungsreihenfolge

Race Conditions in der Praxis

Race Conditions sind nicht nur ein theoretisches Problem, sie haben in der Vergangenheit zu schwerwiegenden Fehlern geführt. Der Therac-25-Unfall in den 1980er Jahren, bei dem Patienten tödliche Strahlendosen erhielten, wurde unter anderem durch eine Race Condition verursacht.

Für Fachinformatiker in der Anwendungsentwicklung ist das Verständnis von Race Conditions besonders wichtig, wenn mit Multithreading, Webservern oder Datenbanken gearbeitet wird. Auch bei der Entwicklung von APIs, die gleichzeitige Anfragen verarbeiten, muss auf thread-sichere Programmierung geachtet werden.

Häufige Fragen.

Bei einer Race Condition laufen die Threads weiter, nur in einer unerwarteten Reihenfolge, was zu falschen oder unvorhersehbaren Ergebnissen führt. Ursache ist zu wenig Synchronisation. Bei einem Deadlock blockieren sich Threads gegenseitig, sodass keiner mehr fortfährt, das Programm hängt. Ursache ist hier zu viel oder falsche Synchronisation. Ironischerweise kann der Versuch, Race Conditions mit Locks zu beheben, neue Deadlocks erzeugen.
Die wichtigsten Techniken sind Locks bzw. Mutexe, die immer nur einen Thread in den kritischen Bereich lassen, und atomare Operationen, die die Hardware als unteilbar garantiert (etwa AtomicInteger in Java). Auch unveränderliche (immutable) Objekte helfen: Was sich nach der Erstellung nicht mehr ändern lässt, kann keine Race Condition auslösen.
Race Conditions treten nur unter bestimmten zeitlichen Bedingungen auf, meist wenn der Scheduler die Threads in einer ungünstigen Reihenfolge ausführt. In den meisten Durchläufen funktioniert das Programm korrekt, weshalb der Fehler sporadisch und schwer reproduzierbar ist. Helfen können Tools wie der Thread Sanitizer, statische Analyse, Code Reviews und Stress-Tests mit vielen gleichzeitigen Threads.

Verwandte Begriffe.

Prüfungsbezug

Passt zu deiner Prüfungsvorbereitung

Wenn dir dieser Begriff in Aufgaben, Projektdokumentation oder Fachgespräch begegnet, ordne ihn direkt in den Prüfungsstoff ein.