Operating systems and concurrency
Race conditions
A race condition happens when the result depends on the order in which threads run. How a read-modify-write loses an update, and why check-then-act bugs are so common.
A race condition is a bug where the outcome depends on the timing of threads or processes. The code may look correct, and each step may be correct on its own. The problem is that another thread can run between steps you assumed were atomic.
The most common shape is read-modify-write. You read a value, compute a new one, and write it back. If two threads do this to the same value, one write can overwrite the other.
Walking through it
Take a bank account with a balance of 100. Two withdrawals run at the same time: one for 30 and one for 50. Each thread does read, compute and write. Here are the steps, in the order that loses money:
balance = 100
a = balance # thread A reads 100
b = balance # thread B reads 100
balance = a - 30 # A writes 70
balance = b - 50 # B writes 50, A's withdrawal is lost
print("lost update:", balance)Output:
lost update: 50The correct balance is 20. The account paid out 80 but lost only 50. Nothing crashed, and every line did what it said. The bug was in the gap between the read and the write.
A counter shows the same thing at scale. The increment counter += 1 compiles to a load, an add and a store. With four threads each doing 100,000 unlocked increments, the total usually comes out short. Wrapping the increment in a lock makes it exact, as shown in the mutexes page.
Check-then-act
The second common shape is check-then-act. You check a condition, then act on it, and the condition changes in between.
if seat.is_free(): # thread A and thread B both see "free"
seat.assign(user_a) # A assigns
seat.assign(user_b) # B also assigns; the seat is double-bookedTicket sales, unique usernames and "create if missing" logic all fail this way. The usual fix is to move the check into the write itself. A database with a unique constraint on (seat_id) lets one insert win and rejects the other. That is more reliable than any in-process check, since it works across machines too.
Warning
A race often hides until traffic grows. A bug that appears once a week under peak load may not show up in a local test with one thread.
Finding races
Race bugs are hard to reproduce, so use tools that find them. A race detector such as Go's -race flag, or ThreadSanitizer in C and C++, instruments memory accesses and reports unsynchronized access to the same location. Run your tests under one of these when your language supports it. A clean run isn't a proof, but it finds the common cases quickly.
Common mistakes
- Locking each read and each write separately, but not the read-modify-write as a single step.
- Checking a condition outside the lock that protects the write.
- Trusting a passing test as evidence of safety.
Check yourself
- Why does a lost update happen even when each thread's code is correct? Answer: two threads read the same old value, and the later write erases the earlier one.
- Why is a database unique constraint safer than an in-process check? Answer: the storage layer makes the check and the write one step, and it works across processes and machines.
Key takeaways
- A race condition is a result that depends on the timing of threads.
- Read-modify-write and check-then-act are the two shapes to watch for.
- Protect the whole operation, or move the check into the write, such as a unique constraint.
- Run race detectors in tests, because timing bugs rarely show on a single run.
Further learning
Next, see the mutex, semaphore and condition variable page for the tools that protect these sections.
Related
Concurrency
Why shared state breaks when two threads touch it at once, and the tools that fix it, from locks and semaphores to atomics, message passing and database constraints.
Mutexes, semaphores and condition variables
The three basic blocking primitives. A mutex gives exclusive access, a semaphore counts available resources, and a condition variable waits for a state change.
Transactions, MVCC and isolation levels
What ACID guarantees on one database node, which concurrency bugs each isolation level still allows, how MVCC gives snapshots without blocking, and the code patterns that keep concurrent writes correct. Start here for transactions.