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.
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.
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.