Part 5 — Concurrency and inter-process communication
Parts 2–4 covered a single process's view of the machine. This part is what happens once multiple processes — or multiple threads inside one — touch the same resource at the same time: race conditions first, then the IPC mechanisms (pipes, signals, shared memory) that let them cooperate on purpose instead of corrupting each other by accident.
Chapter 17 — Race conditions at the OS level
In one sentence
A race condition happens when the correctness of a program depends on the precise TIMING or ORDER of events that the OS does not guarantee — and because that timing can vary run to run, the bug can be invisible in testing and catastrophic in production.
Why this matters
Race conditions are one of the hardest classes of bug to find, because they often don't reproduce reliably — "it worked when I tested it" is exactly what you'd expect from a race condition, not evidence it's fixed.
How it actually works
THE OS MAKES FEW TIMING GUARANTEES. When two threads (Chapter 7) or
processes run "at the same time," the scheduler (Chapter 8) decides
the EXACT interleaving of their instructions, and that interleaving
can change from run to run -- based on system load, CPU count,
scheduling decisions that have nothing to do with your program's
logic.
THE CLASSIC SHAPE OF A RACE CONDITION -- a "check then act" sequence
that ISN'T ATOMIC (isn't guaranteed to happen as one uninterruptible
step):
Thread A: reads shared_counter (sees 5)
Thread B: reads shared_counter (ALSO sees 5, before A writes back)
Thread A: writes shared_counter = 5 + 1 = 6
Thread B: writes shared_counter = 5 + 1 = 6 <- should be 7!
Both threads read the SAME starting value because the read-modify-
write wasn't protected -- one of the two increments is SILENTLY LOST.
This can happen with shared memory (Chapter 7's threads), with
files (two processes both checking "does this file exist" then
both creating it), or with any shared, mutable resource touched from
more than one thread of control without coordination.
THIS IS WHY THIS GUIDE KEEPS RETURNING TO "ATOMIC" OPERATIONS
(Chapter 18's locks, and this same theme appears throughout the
companion security guide's race-condition chapter, in a web-request
context) -- the fix is always some mechanism that makes the
check-and-act sequence happen as ONE step nothing else can interleave
with.
Real-world example
The Therac-25, a radiation therapy machine built in the 1980s, is
the canonical, deeply-studied real-world case for why race conditions
are taken so seriously in safety-critical engineering. Investigated
in exhaustive detail by Nancy Leveson and Clark Turner (their 1993
report remains the standard reference, taught in most software-
engineering curricula), the machine's control software had a RACE
CONDITION: if an operator entered treatment parameters QUICKLY enough
-- specifically, by using keyboard edit commands to correct a mistake
within about 8 seconds -- the software's checks for a dangerous
configuration could be BYPASSED, because a shared variable
(essentially a flag indicating whether safety checks had completed)
could be read by one part of the program before another part had
finished updating it. Between 1985 and 1987, this bug (and related
issues in the same system) contributed to several patients receiving
massive radiation overdoses, with fatal and severely injurious
consequences.
THE LESSON FOR THIS CHAPTER, stated as starkly as the real history
warrants: the bug was HARD TO REPRODUCE (it depended on precise,
fast operator timing, which most testers and most routine use never
triggered) and existed in FIELDED, IN-USE equipment for YEARS before
it was understood. "It passed testing" is not evidence a race
condition doesn't exist -- it's evidence the specific interleaving
that triggers it didn't happen to occur during testing.
Try it yourself
# See a race condition happen live, harmlessly, in a tiny script
cat > race.py << 'EOF'
import threading
counter = 0
def increment():
global counter
for _ in range(100000):
counter += 1 # NOT atomic: read, add 1, write back -- 3 steps
threads = [threading.Thread(target=increment) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(f"expected 400000, got {counter}")
EOF
python3 race.py # run it a few times -- the result varies, and is
# usually LESS than 400000, because increments were lost
Note: Python's Global Interpreter Lock (GIL) means this specific
demo's "lost update" rate may be lower/less dramatic than the same
code in a language without a GIL (like Go, Java, or C) -- but the
race condition is real in ALL of them; only its VISIBILITY differs.
Common mistakes
- "It worked every time I tested it, so it's fine." This is exactly what a race condition looks like from the outside — absence of failure in testing is weak evidence, not proof.
- Assuming a single line of code is "one step" for the CPU/OS. Even
counter += 1is actually several separate machine instructions (read, add, write) — any of them can be interrupted between. - Only worrying about race conditions in explicitly multi-threaded code. Two separate PROCESSES racing on a shared file, a shared database row, or a shared external resource is exactly the same class of bug.
Check yourself
- What makes a sequence of operations "not atomic," and why does that matter?
- Why can a race condition pass testing repeatedly and still be a real bug?
- In the Therac-25 case, what made the bug so hard to catch before it caused real harm?
(Answers: Appendix E.)
Further reading
- Report: Leveson & Turner, "An Investigation of the Therac-25 Accidents" (IEEE Computer, 1993) — the definitive, sobering account; freely findable online.
- Book (free): OSTEP, "Concurrency: An Introduction" chapter.
- Guide: this wiki's security guides cover the same underlying idea in a web-application context (check-then-act request races) if you want the application-layer version of this exact lesson.