Why Concurrency Breaks Naive Designs
A race condition occurs when the correctness of a program depends on the relative timing
of two or more threads. The seat-booking scenario is a standard example. Two customers open
the booking page for the last seat on a flight at the same moment. The reserve() method
reads available = 1, decides the seat is free, and confirms the reservation — for both
threads. Both customers receive a confirmation email, but there is only one seat.
No single line of that code is incorrect in isolation. The defect lies in the gap between the read and the write. A second thread can enter that gap, read the same stale value, and make the same decision. This is a race condition: the outcome depends on which thread executes first, and the design provides no mechanism to prevent the dangerous interleaving.
Concurrency does not introduce new kinds of operations. It introduces new orderings of the same operations, and many of those orderings produce results no single-threaded test will catch. This article explains the mechanism behind race conditions and the vocabulary used to describe and fix them.
The anatomy of a race condition
Consider a simplified seat-booking service. The method is short and straightforward, but it is unsafe under concurrent access.
1# Bad: check-then-act race — two threads can both read available=1 and both proceed
2class SeatInventory:
3 def __init__(self, total: int) -> None:
4 self._available = total
5
6 def reserve(self, seat_id: str) -> bool:
7 if self._available > 0: # Thread A reads available=1 ──┐
8 # Thread B also reads available=1 here ◄───────────────┘
9 self._available -= 1 # Thread A writes available=0
10 # Thread B writes available=-1 ← DOUBLE-BOOKED
11 return True
12 return False
Seeing the interleaving
The sequence diagram below makes the dangerous ordering explicit. Neither thread does anything unusual — the race is entirely a product of timing.
When a read is followed by a conditional write, the relevant question is: what happens if another thread arrives between those two steps? If the answer is an incorrect outcome, the code contains a check-then-act race.
Critical section and atomicity
The pair of operations — read the count, write the new count — must happen as a single, indivisible unit. In concurrency terminology, that indivisible unit is called atomic. The region of code that must not be executed by more than one thread at a time is called the critical section.
A critical section has three properties that make it safe. First, mutual exclusion: only one thread executes the section at any moment. Second, progress: a thread not in the section cannot prevent others from entering. Third, bounded waiting: no thread waits forever while others cycle through the section repeatedly.
The standard mechanism to enforce mutual exclusion is a lock (also called a mutex — mutual exclusion object). A thread acquires the lock before entering the critical section and releases it afterward. Any other thread that tries to acquire an already-held lock blocks until the first thread releases it.
1import threading
2
3# Good: lock wraps the entire check-then-act so it executes atomically
4class SeatInventory:
5 def __init__(self, total: int) -> None:
6 self._available = total
7 self._lock = threading.Lock()
8
9 def reserve(self, seat_id: str) -> bool:
10 with self._lock: # only one thread enters at a time
11 if self._available > 0:
12 self._available -= 1
13 return True
14 return False
What changes with the lock
The lock closes the gap. When Thread A holds the lock and is inside the critical section,
Thread B blocks at the acquire call. It does not read available until Thread A has
finished both the check and the decrement and released the lock. By the time Thread B reads,
the count is accurate.
Thread B now sees the updated value and correctly turns away. One seat, one confirmation.
The vocabulary in use
Three terms appear in every concurrency discussion, and precise use of them signals maturity to an interviewer.
A race condition is a defect whose presence or absence depends on the relative timing of two or more threads. It may not reproduce on every run — it often appears only under load — which makes it difficult to detect through ordinary testing. The seat-booking defect above is a race condition because the double-booking only occurs when Thread B enters the read before Thread A finishes the write.
A critical section is a block of code that accesses shared mutable state and must therefore execute atomically. Identifying your critical sections — and keeping them as short as possible — is the first design skill in concurrent code. Every millisecond a lock is held is a millisecond another thread is blocked.
Atomicity means all-or-nothing execution: either every operation in the critical section completes, or it appears as though none of them did. A lock enforces atomicity by preventing interleaving. Hardware instructions such as compare-and-set provide atomicity without a lock on a single memory word — a technique the next article explores.
Interview framing
When an interviewer adds "now assume multiple users can book at the same time," they are
checking whether you recognize the check-then-act pattern and can identify the critical
section. A complete answer names the race explicitly ("two threads can both see
available = 1"), identifies the critical section (the read-check-write block), and
proposes a fix before writing any code.
A lock is one valid answer, but not the only one. The next articles cover atomic compare-and-set and optimistic database locking, which avoid blocking entirely. The appropriate tool depends on whether the shared state lives in a single process, across multiple processes, or in a database.