Wat is Data race?

Situatie waarin meerdere threads zonder synchronisatie tegelijk dezelfde variabele benaderen en minstens één daarvan schrijft. Het resultaat verschilt per uitvoering. In C en C++ is het ongedefinieerd gedrag.

Japanse benaming: データ競合

“Data race” komt voor in 4 oefenvragen uit 1 onderwerpen, bijv. Multicore en synchronisatie.

Vakgebied: Multicore en synchronisatie

Hoe een update verloren gaat

De opdracht counter++ lijkt atomair, maar wordt gecompileerd tot drie stappen: de waarde laden, er 1 bij optellen en terugschrijven. Stel dat twee threads elk 100.000 keer een gedeelde teller verhogen. Als thread A 100 laadt en thread B ook 100 laadt voordat A heeft geschreven, schrijven beide 101 en verdwijnt er een verhoging. Afhankelijk van hoe de threads door elkaar lopen, kan de eindwaarde overal onder de 200.000 liggen en verschilt ze per uitvoering. Er crasht niets, dus de bug wordt gemakkelijk over het hoofd gezien, en hij kan verdwijnen als het programma wordt vertraagd door een debugger of logging.

Data race vs. race condition

Een data race is precies gedefinieerd: twee threads benaderen gelijktijdig dezelfde geheugenlocatie, minstens één toegang is een schrijfactie en er is geen synchronisatie die ze ordent. Een race condition is breder: elke bug waarvan de uitkomst van timing afhangt. De twee overlappen, maar zijn niet identiek. Een check-then-act-reeks die uit afzonderlijk atomaire bewerkingen bestaat, heeft geen data race, maar kan toch een race condition zijn, omdat een andere thread tussen de controle en de actie iets kan doen. Talen gaan verschillend met data races om: in C en C++ heeft een programma met een data race ongedefinieerd gedrag, terwijl Java zulke programma's een gedefinieerde maar zwakke semantiek geeft.

De oplossing: locks en atomics

De remedie is de conflicterende toegangen een volgorde opleggen. Een lock (mutex) maakt van de update een kritieke sectie die maar door één thread tegelijk wordt uitgevoerd; kritieke secties kort houden beperkt het wachten. Voor eenvoudige bewerkingen zijn atomaire instructies sneller: fetch-and-add, zoals lock xadd op x86 of amoadd.w op RISC-V, voert lezen, optellen en schrijven ondeelbaar uit en geeft de oude waarde terug, zodat het nooit opnieuw hoeft te proberen. Complexere updates gebruiken compare-and-swap-lussen. Een variabele volatile declareren in C of C++ voorkomt geen data race, omdat het noch atomiciteit noch ordening tussen threads oplevert.

Waarom compilers en hardware races erger maken

Omdat C en C++ uitgaan van programma's zonder races, mogen compilers een gedeelde variabele in een register houden, een load buiten een lus tillen of stores samenvoegen, zodat een thread die in een lus wacht op een vlag die een andere thread zonder synchronisatie schrijft, de wijziging misschien nooit ziet. Hardware voegt daar via store buffers en caches haar eigen herordening aan toe. Synchronisatiebewerkingen, zoals het verkrijgen van een lock en atomics met de juiste memory ordering, vertellen zowel de compiler als de hardware waar de volgorde behouden moet blijven. Dynamische race-detectors zoals ThreadSanitizer helpen races te vinden die je met testen alleen zelden aan het licht brengt.

Nu oefenen: vragen over Data race

Welke uitleg over een data race is juist?

  1. AEen toestand waarin meerdere threads dezelfde variabele alleen tegelijk lezen
  2. BEen toestand waarin één thread twee keer achtereen naar dezelfde variabele schrijft
  3. CEen toestand waarin meerdere threads zonder synchronisatie tegelijk dezelfde variabele benaderen en minstens één daarvan schrijft
Antwoord en toelichting tonen

Antwoord: C. Een toestand waarin meerdere threads zonder synchronisatie tegelijk dezelfde variabele benaderen en minstens één daarvan schrijft

Een data race is een toestand waarin de volgorde van lezen en schrijven op dezelfde plaats niet door synchronisatie is vastgelegd, zodat het resultaat afhangt van de volgorde van uitvoering. Omdat het afhankelijk van timing wel of niet optreedt, is het een fout die bij tests moeilijk te vinden is. In C en C++ is het gedrag van een programma met een data race ongedefinieerd. De volgorde moet worden vastgelegd met locks of atomaire variabelen.

Veelgestelde vragen

Wat is het verschil tussen een data race en een race condition?

Een data race is gelijktijdige, niet-gesynchroniseerde toegang tot dezelfde geheugenlocatie waarbij minstens één toegang een schrijfactie is. Een race condition is elke timingafhankelijke bug, die ook kan bestaan als elke afzonderlijke toegang correct gesynchroniseerd is.

Kan een data race optreden op een CPU met één core?

Ja. Als het besturingssysteem een thread onderbreekt (preemption) tussen zijn load en store, kan een andere thread draaien en dezelfde variabele bijwerken, zodat een verhoging ook zonder echte parallelle uitvoering verloren kan gaan.

Voorkomt het sleutelwoord volatile data races in C en C++?

Nee. Volatile dwingt de compiler alleen om elke toegang daadwerkelijk uit te voeren; het maakt read-modify-write-reeksen niet atomair en ordent ze niet tussen threads. Gebruik in plaats daarvan atomics of locks.

“Data race” in de toelichting

Vragen met “Data race” (4)

  1. Welke uitleg over een data race is juist?
  2. Wanneer bovenstaande code door twee threads tegelijk wordt uitgevoerd, kan de uiteindelijke waarde van counter kleiner zijn dan 200000. Wat is de juiste reden?
  3. Welke beschrijving van de werking van een fetch-and-add-instructie, zoals amoadd.w van RISC-V of lock xadd van x86, is juist?
  4. Welke uitleg over een kritieke sectie is juist?

Verwante termen

Kritieke sectie (critical section) ABA-probleem Acquire-release Atomaire instructie Barrièresynchronisatie Compare-and-swap Conditievariabele Deadlock Directorygebaseerde coherentie (directory-based) False sharing

Naar de woordenlijst