4.2.13Operating Systems

Race condition — example, why it's a problem

1,964 words9 min readdifficulty · medium3 backlinks

WHAT is a race condition?

Three ingredients must ALL be present:

  1. Shared data (a variable, file, counter, struct).
  2. Concurrent access (≥2 threads/processes running interleaved).
  3. At least one writer (read-only sharing is always safe).

If any one is missing → no race.


WHY does it happen? (first-principles derivation)

The key insight: a line like counter++ is not atomic. The CPU cannot increment memory in one indivisible step. The compiler breaks it into three machine instructions:

counter++    LOAD Rcounterread  ;  ADD RR+1modify  ;  STORE counterRwrite\texttt{counter++} \;\equiv\; \underbrace{\texttt{LOAD } R \leftarrow counter}_{\text{read}} \;;\; \underbrace{\texttt{ADD } R \leftarrow R+1}_{\text{modify}} \;;\; \underbrace{\texttt{STORE } counter \leftarrow R}_{\text{write}}

This is a read–modify–write (RMW) sequence. Each thread has its own private register RR. The OS scheduler may preempt a thread at any point — including between these three steps.


The canonical EXAMPLE

Two threads each run counter++ 1 time, starting from counter = 5. Expected final value: 77.

Figure — Race condition — example, why it's a problem

WHY it's a problem (not just "wrong sometimes")

HOW we fix it: make the critical section mutually exclusive (only one thread inside at a time) using a lock/mutex, or use an atomic instruction so the RMW becomes one indivisible step:

lock(m); counter++; unlock(m);     // mutual exclusion
// or
atomic_fetch_add(&counter, 1);     // hardware-atomic RMW

Common mistakes (Steel-man + fix)


Flashcards

What three ingredients are all required for a race condition?
Shared mutable state + concurrent access + at least one writer.
Why is counter++ not atomic?
It compiles to a read–modify–write: LOAD, ADD, STORE — three separate interruptible machine instructions, each thread using its own register.
What is a critical section?
The region of code that accesses shared state and must run with mutual exclusion.
In the lost-update example with two counter++ starting at 5, what wrong value can appear and why?
6 instead of 7 — both threads LOAD 5 before either STOREs, so the second STORE overwrites the first, losing one increment.
Why doesn't passing tests 1000 times prove a race is absent?
Races are probabilistic; with hit probability p per run, P(no failure)=(1−p)^n can stay high even when the bug is real.
Why does volatile NOT fix a counter race?
It only prevents compiler register-caching; it neither makes RMW atomic nor provides mutual exclusion.
What is a TOCTOU race?
Time-Of-Check-To-Time-Of-Use: state changes between checking a condition and acting on it (e.g. file permission check then open), exploitable for security.
For N unsynchronized counter++ from 0, what is the range of possible final values?
Between 2 and N — correct is N, but adversarial interleavings can drop it as low as 2.
Two correct ways to eliminate the counter race?
(1) Mutual exclusion via lock/mutex around the critical section; (2) a hardware-atomic RMW like atomic_fetch_add.
Why is a race called a "Heisenbug"?
Observing/slowing it (e.g. adding prints) changes the timing and can make the bug disappear, so it dodges debugging.

Recall Feynman: explain to a 12-year-old

Imagine you and a friend share one jar of cookies and a notepad that says how many are left: 5. You both read "5", both take one, and both want to update the note to "4" (5−1). But updating means: read 5 → think "4" → write 4. If your friend reads "5" while you're still thinking, they also write "4". Two cookies are gone, but the note still says 4 instead of 3. The note is wrong — and whether it goes wrong depends on who looks at the same moment. The fix: put a lock on the notepad so only one person can read-and-write it at a time.

Connections

Concept Map

ingredient

ingredient

ingredient

touches

expands to

interrupts

causes

leads to

produces

final value depends on timing

Race condition

Shared mutable state

Concurrent access ≥2 threads

At least one writer

Read-modify-write not atomic

Scheduler preempts mid-RMW

Thread reads stale value

Lost update

Critical section

Non-deterministic result

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Dekho, race condition ka matlab simple hai: do thread ek hi shared variable ko ek saath chhoo rahe hain, aur kam se kam ek thread usme likh raha hai (write). Problem yahan aati hai kyunki counter++ jaisi ek line bhi CPU ke liye ek step nahi hai — wo teen instructions me tootti hai: pehle value LOAD karo register me, phir ADD karke 1 badhao, phir wapas STORE karo. Beech me OS ka scheduler kabhi bhi thread ko rok kar doosri ko chala sakta hai.

Maan lo counter = 5 hai. Thread A ne 5 LOAD kiya, abhi STORE nahi kiya. Tabhi Thread B aa gaya aur usne bhi purana 5 hi LOAD kar liya. Dono 6 banayenge aur dono 6 hi STORE karenge. Result aaya 6, jabki hona chahiye tha 7 — ek increment gayab ho gaya. Isko "lost update" kehte hain. Sabse khatarnak baat: yeh bug har baar nahi aata, timing pe depend karta hai, isliye testing me chhup jaata hai aur production me load aane par phat jaata hai.

Isiliye ise "Heisenbug" bhi bolte hain — print laga do to slow ho ke bug chhup jaata hai. Aur agar tum sochte ho "1000 baar test pass ho gaya to safe hai" — galat, kyunki probability (1-p)^n se clean run aa sakta hai bug hone par bhi. Fix kya hai? Critical section ko lock/mutex se mutual exclusion do, taaki ek time pe ek hi thread andar ho, ya phir atomic operation use karo jisse poora read-modify-write ek hi indivisible step ban jaye. Yaad rakho: volatile se yeh theek nahi hota — wo sirf compiler caching rokta hai, atomicity nahi deta.

Test yourself — Operating Systems

Connections