Concurrency: race conditions, mutexes, and semaphores
Two threads incrementing the same counter, each doing something as simple as reading a number and adding one to it, can still lose updates and produce a wrong final answer — because "read, then add one" is not one atomic step, and the OS never promised it would be.
Intermediate
3 min read
Why sharing memory between threads is dangerous
Lesson 2 established that threads within a process share memory. That's
what makes a race condition possible: two or more threads accessing
shared data concurrently, where the final result depends on the precise,
unpredictable timing of which thread does what, when — instead of on the
program's actual logic.
A concrete example: the lost update
counter += 1 looks like a single operation, but at the hardware level it's
really three steps: read the current value, add one, write the new value
back. If two threads both run this on the same shared counter, their steps
can interleave:
Thread A reads counter (0)
Thread B reads counter (0) <- both read the SAME starting value
Thread A computes 0 + 1 = 1, writes 1
Thread B computes 0 + 1 = 1, writes 1 <- overwrites A's update
Final value: 1 (should have been 2)
Neither thread did anything individually wrong — the bug exists entirely in
the interleaving, which is why race conditions are notoriously hard to
reproduce: the exact timing needed to trigger one might only happen rarely,
under specific load, making the bug appear and disappear seemingly at
random.
The mutex: mutual exclusion
A mutex (mutual exclusion lock) ensures only one thread can execute a
protected section of code — a critical section — at a time. A thread
must acquire the mutex before entering the critical section and release it
after; any other thread trying to acquire it while it's held simply waits.
Wrapping the counter example's read-add-write sequence in a mutex makes the
whole sequence effectively atomic from every other thread's point of view,
eliminating the interleaving that caused the lost update.
The semaphore: a counter-based generalization
A semaphore is a more general synchronization primitive holding a
count, not just a locked/unlocked boolean. A mutex is really a semaphore
whose count is capped at 1 (allowing exactly one thread through at a time).
A semaphore initialized to N allows up to N threads through concurrently —
useful for limiting concurrent access to a resource with a genuine capacity
above one, like a fixed-size connection pool with room for exactly N active
connections at once.
The cost of getting this wrong in either direction
Too little synchronization produces race conditions — silent data
corruption, since the program keeps running, just with a wrong result.
Too much (locking far more than necessary, or holding locks for far longer
than needed) produces contention — threads spending most of their time
waiting for locks instead of doing real work, which can make a
multi-threaded program slower than an equivalent single-threaded one. The
goal is protecting exactly the shared data that needs it, for exactly as
long as needed, and nothing more.