Kategorie: runtimeSchwierigkeit: FortgeschrittenVeröffentlicht: 2024-12-18
Race Conditions verstehen und verhindern
Race Conditions treten auf, wenn mehrere asynchrone Operationen um gemeinsame Ressourcen konkurrieren, was zu unvorhersehbarem Verhalten führt. In JavaScript sind diese besonders häufig in Anwendungen mit mehreren API-Aufrufen, Statusaktualisierungen oder Benutzerinteraktionen.
Häufige Race Condition Szenarien
1. Mehrere API-Aufrufe
// Problem: Ergebnisse können in anderer Reihenfolge eintreffen als Anfragen async function holeBenutzerdaten(benutzerId) { const profil = await fetch(`/api/profil/${benutzerId}`); const beitraege = await fetch(`/api/beitraege/${benutzerId}`); return { profil: await profil.json(), beitraege: await beitraege.json() }; } // Lösung: Promise.all für gleichzeitige Anfragen verwenden async function holeBenutzerdatenFixed(benutzerId) { const [profil, beitraege] = await Promise.all([ fetch(`/api/profil/${benutzerId}`).then(res => res.json()), fetch(`/api/beitraege/${benutzerId}`).then(res => res.json()) ]); return { profil, beitraege }; }
2. Statusaktualisierungen in React
// Problem: Mehrere setState-Aufrufe können zu Race Conditions führen function Zaehler() { const [zaehler, setZaehler] = useState(0); const erhoehen = () => { setZaehler(zaehler + 1); // Verwendet veralteten 'zaehler' setZaehler(zaehler + 1); // Verwendet denselben veralteten 'zaehler' }; // Lösung: Funktionale Updates verwenden const erhoehenFixed = () => { setZaehler(vorher => vorher + 1); // Verwendet aktuellsten Status setZaehler(vorher => vorher + 1); // Verwendet aktuellsten Status }; return <button onClick={erhoehenFixed}>{zaehler}</button>; }
3. Verzögerte API-Aufrufe
// Problem: Mehrere schnelle Aufrufe können Ergebnisse in falscher Reihenfolge zurückgeben function Suchkomponente() { const [ergebnisse, setErgebnisse] = useState([]); async function suche(anfrage) { const antwort = await fetch(`/api/suche?q=${anfrage}`); const daten = await antwort.json(); setErgebnisse(daten); // Könnte veraltete Ergebnisse setzen } // Lösung: Letzte Anfrage verfolgen function SuchkomponenteFixed() { const [ergebnisse, setErgebnisse] = useState([]); const letzteAnfrage = useRef(''); async function suche(anfrage) { letzteAnfrage.current = anfrage; const antwort = await fetch(`/api/suche?q=${anfrage}`); const daten = await antwort.json(); // Nur aktualisieren, wenn dies noch die letzte Anfrage ist if (letzteAnfrage.current === anfrage) { setErgebnisse(daten); } } return <input onChange={e => suche(e.target.value)} />; } }
Präventionsstrategien
1. Anfragenabbruch
function SucheMitAbbruch() { const [ergebnisse, setErgebnisse] = useState([]); const abbruchController = useRef(null); async function suche(anfrage) { // Vorherige Anfrage abbrechen if (abbruchController.current) { abbruchController.current.abort(); } // Neuen Abbruch-Controller erstellen abbruchController.current = new AbortController(); try { const antwort = await fetch(`/api/suche?q=${anfrage}`, { signal: abbruchController.current.signal }); const daten = await antwort.json(); setErgebnisse(daten); } catch (fehler) { if (fehler.name === 'AbortError') { // Anfrage wurde abgebrochen, ignorieren return; } throw fehler; } } return <input onChange={e => suche(e.target.value)} />; }
2. Anfrage-Warteschlange
class AnfrageWarteschlange { constructor() { this.warteschlange = []; this.verarbeitung = false; } async hinzufuegen(anfrage) { return new Promise((resolve, reject) => { this.warteschlange.push({ anfrage, resolve, reject }); this.naechsteVerarbeiten(); }); } async naechsteVerarbeiten() { if (this.verarbeitung || this.warteschlange.length === 0) { return; } this.verarbeitung = true; const { anfrage, resolve, reject } = this.warteschlange.shift(); try { const ergebnis = await anfrage(); resolve(ergebnis); } catch (fehler) { reject(fehler); } finally { this.verarbeitung = false; this.naechsteVerarbeiten(); } } } // Verwendung const warteschlange = new AnfrageWarteschlange(); async function verarbeiteInReihenfolge(id) { return warteschlange.hinzufuegen(async () => { const antwort = await fetch(`/api/daten/${id}`); return antwort.json(); }); }
3. Mutex-Implementierung
class Mutex { constructor() { this.warteschlange = []; this.gesperrt = false; } async acquire() { return new Promise(resolve => { if (!this.gesperrt) { this.gesperrt = true; resolve(); } else { this.warteschlange.push(resolve); } }); } release() { if (this.warteschlange.length > 0) { const naechster = this.warteschlange.shift(); naechster(); } else { this.gesperrt = false; } } } // Verwendung const mutex = new Mutex(); async function kritischerBereich() { await mutex.acquire(); try { // Synchronisierte Operationen ausführen await synchronisierteOperation(); } finally { mutex.release(); } }
Race Conditions testen
describe('Race Condition Tests', () => { it('sollte gleichzeitige Anfragen korrekt behandeln', async () => { const promises = []; const ergebnisse = new Set(); // Mehrere gleichzeitige Anfragen erstellen for (let i = 0; i < 10; i++) { promises.push( holeDaten(i).then(ergebnis => { ergebnisse.add(ergebnis); }) ); } await Promise.all(promises); // Ergebnisse überprüfen expect(ergebnisse.size).toBe(10); }); it('sollte Reihenfolge mit Anfrage-Warteschlange beibehalten', async () => { const warteschlange = new AnfrageWarteschlange(); const ergebnisse = []; // Anfragen in bestimmter Reihenfolge hinzufügen await Promise.all([ warteschlange.hinzufuegen(() => { ergebnisse.push(1); return verzoegern(100); }), warteschlange.hinzufuegen(() => { ergebnisse.push(2); return verzoegern(50); }), warteschlange.hinzufuegen(() => { ergebnisse.push(3); return verzoegern(10); }) ]); // Reihenfolge überprüfen expect(ergebnisse).toEqual([1, 2, 3]); }); });
Best Practices
-
Atomare Operationen verwenden
// Problem: Nicht-atomare Operation let zaehler = 0; async function erhoehen() { const wert = zaehler; await verzoegern(100); zaehler = wert + 1; } // Lösung: Atomare Operationen verwenden const zaehler = new AtomicInteger(0); async function erhoehen() { zaehler.incrementAndGet(); }
-
Wiederholungslogik implementieren
async function fetchMitWiederholung(url, optionen = {}, versuche = 3) { try { return await fetch(url, optionen); } catch (fehler) { if (versuche > 0) { await verzoegern(1000); return fetchMitWiederholung(url, optionen, versuche - 1); } throw fehler; } }
-
Versionsnummern verwenden
function VersionierterStatus() { const [status, setStatus] = useState({ daten: null, version: 0 }); async function datenAktualisieren(neueDaten) { const neueVersion = status.version + 1; const antwort = await verarbeiteDaten(neueDaten); // Nur aktualisieren, wenn Version übereinstimmt setStatus(aktuell => { if (aktuell.version === neueVersion - 1) { return { daten: antwort, version: neueVersion }; } return aktuell; }); } }
Debug-Werkzeuge
-
Chrome DevTools Netzwerk-Panel
- Netzwerk-Panel zur Beobachtung der Anfrage-Zeitplanung nutzen
- "Langsames 3G" aktivieren, um Race Conditions sichtbarer zu machen
- Anfragen-Blocking für Fehlersimulation verwenden
-
Performance-Profiling
console.time('operation'); await fuehreOperationAus(); console.timeEnd('operation');
-
Anfragen-Logging
class AnfragenLogger { static log(methode, url, startZeit) { const dauer = Date.now() - startZeit; console.log(`${methode} ${url} in ${dauer}ms abgeschlossen`); } static async wrap(promise, methode, url) { const startZeit = Date.now(); try { return await promise; } finally { this.log(methode, url, startZeit); } } }
Präventions-Checkliste
-
Designphase
- Gemeinsame Ressourcen identifizieren
- Synchronisationspunkte planen
- Race-Condition-Risiken dokumentieren
-
Implementierungsphase
- Geeignete Synchronisationsprimitiven verwenden
- Anfragenabbruch implementieren
- Versionsverfolgung hinzufügen
-
Testphase
- Gleichzeitige Testszenarien erstellen
- Netzwerk-Drosselung verwenden
- Mit verschiedenen Timing-Bedingungen testen