Two operations accept a booking when only one seat remains. Both check that the remaining count is 1, and both confirm the booking, exceeding capacity. Even when individual reads and writes work correctly, another operation can intervene between a check and an update and cause this kind of error.
What you will learn
You will lay out read, compute, and write steps and explain how an update can be lost. You will distinguish making one variable operation atomic from protecting a sequence of operations, and choose the scope of synchronization from the condition that must remain true.
Breaking a one-line update into steps
Interleaving means that the steps of multiple executions are mixed together. Even the single line counter += 1 can be viewed conceptually as reading the value, adding one, and writing the result. A source-code line is not automatically a boundary that other execution cannot enter.
Initial value: counter = 0
A: read counter → 0
B: read counter → 0
A: calculate 0 + 1 and write 1
B: calculate 0 + 1 and write 1
Final value: counter = 1 (1 of the 2 increments was lost)This is an abstract execution illustrating a lost update. Real language implementations also impose optimization and memory-model rules. In particular, conflicting unsynchronized accesses to an ordinary variable in C or C++ can create a data race and cause undefined behavior. Its outcomes cannot be predicted simply by listing values such as 0, 1, and 2. Keep explanatory step sequences separate from what the implementation language actually guarantees.
Race conditions and data races have different scopes
A race condition is a problem in which the timing or order of execution changes whether the desired result is achieved. A data race concerns conditions defined by a language's memory model, such as unsynchronized conflicting accesses to shared memory. Even if every individual memory access is atomic, a logical race can remain between checking a booking and confirming it.
A lock encloses the whole sequence that must be protected
from threading import Lock
lock = Lock()
remaining = 1
def reserve():
global remaining
with lock:
if remaining == 0:
return False
remaining -= 1
return TrueThis example assumes threads within one process call reserve using the same lock. Checking and decrementing the remaining count both occur inside the lock, so another participating thread cannot enter midway. The invariant is that the remaining count must not become negative. Other code that modifies remaining without using the same lock can break this guarantee.
Locking only the read and releasing the lock before decrementing would not solve the original booking problem. At the other extreme, keeping a large lock held while waiting for network communication can block unrelated work. Start by minimizing shared state and enclosing the operations needed to preserve the invariant. Bookings spanning multiple processes or servers require a mechanism appropriate to that wider scope.
Atomic operations do not automatically form an atomic sequence
Atomicity means that an operation can be treated as one indivisible action, without an observable intermediate state. An atomic increment prevents an update from being lost between the read, calculation, and write. However, an atomic read followed by a condition check and a separate atomic write does not make the combined sequence atomic.
An atomic increment may be appropriate for one counter. An operation that must keep an account balance consistent with a transfer record needs protection across multiple pieces of state. Rather than choosing by the name of a synchronization primitive, write down which intermediate states others must not observe and which conditions must hold after the operation.
Check your understanding
Question 1: Can a race condition occur on a single core? Yes. If execution switches from A to B after A's check but before its update, both can make decisions using the old state. Simultaneous execution is not required.
Question 2: The remaining-seat read and write are individually atomic. Does that always prevent double booking? No. The whole sequence from checking to reserving must be protected. Choose a lock, a conditional update, or another mechanism that matches the full operation.
Processes, Threads, and Context Switches explains why switching CPU execution alone is enough for steps to interleave.