Module 10
Concurrency
The vocabulary first, because precision here is the signal. A process owns its own memory; threads live inside one process and share its memory — cheap to create, cheap to communicate, and dangerous for exactly the same reason. Concurrency is dealing with many things in flight (even interleaved on one core); parallelism is literally executing at the same instant on multiple cores. A function is thread-safewhen concurrent callers can't corrupt its state or observe it half-updated.
The central hazard is the race condition: correctness that depends on timing. The canonical example is counter += 1, which compiles to read, add, write — three steps. Two threads interleave those steps, one writes a value computed from a stale read, and an update silently vanishes. The bug is probabilistic: it passes every test on your machine and corrupts data under production load. Reproduce it on demand below.
🎛 Race-condition simulator
Two workers each run counter += 1 a thousand times. Expected final value: 2000.
Run it a few times — unlocked results change every run. That nondeterminism is the defining smell of a race.
counter += 1is secretly three steps — read, add, write. When two workers interleave those steps, one writes a value computed from a stale read and the other's update vanishes. The lock makes read-modify-write atomic: slower, but correct every single run.
The fixes, in the order to reach for them
- Don't share — give each worker its own accumulator and merge at the end; immutable data can be read by any number of threads with no locks at all.
- Lock (mutex) — wrap the read-modify-write in a critical section so only one thread runs it at a time. Correct, simple; costs throughput and introduces deadlock risk.
- Atomics — hardware compare-and-swap for single counters/flags: lock-free and exact for simple cases.
- Hand work to a queue — many producers enqueue, one consumer owns the state; most practical systems quietly converge on this shape.
Deadlock — the other classic
Thread 1 holds lock A, wants B; thread 2 holds B, wants A. Both wait forever. Four conditions must all hold (mutual exclusion, hold-and-wait, no preemption, circular wait) — and the practical prevention is breaking the last one: every thread acquires locks in the same global order. If asked to design around it, that one sentence is most of the answer; lock timeouts and single-lock designs are the fallbacks.
Related terms worth recognizing: livelock (threads keep acting but make no progress — each politely yields to the other), starvation (one thread never gets scheduled), and condition variable / semaphore (wait efficiently until a state holds / limit concurrent holders to n).
Say it out loud
- “+= is read-modify-write — not atomic — so concurrent increments lose updates; I'd use an atomic counter or a lock.”
- “This object is immutable, so it's safely shareable with zero synchronization.”
- “Two locks means a global acquisition order, or we're one unlucky interleaving from deadlock.”
- “I'd keep the critical section tiny — compute outside, lock only around the shared write.”