Facebook Pixel

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.

Invest in Yourself
Your new job is waiting. 83% of people that complete the program get a job offer. Unlock unlimited access to all content and features.
Go Pro