Skip to main content
Low-Level Design · lesson 15 of 18
Open the Low-Level Design page →
Low-Level Design advanced Lesson 15 of 18

Concurrency in LLD: A Real Race Condition in the Parking Lot, Then the Fix

Two threads check the same spot, both see it free, and both take it — a real double-booking bug reproduced on purpose, then closed with a threading.Lock.

By SkillByExample · Published

lld oop python concurrency threading

Every LLD example so far has run single-threaded — real systems don’t get that luxury.

The bug: two threads, one spot, both win

import threading
import time

class UnsafeParkingLot:
    def __init__(self):
        self.occupied = False

    def allocate(self, vehicle_id, results):
        if not self.occupied:
            time.sleep(0.05)  # simulates work between checking and acting
            self.occupied = True
            results.append(f"{vehicle_id} got the spot")
        else:
            results.append(f"{vehicle_id} found the spot full")

lot = UnsafeParkingLot()
results = []
t1 = threading.Thread(target=lot.allocate, args=("car-1", results))
t2 = threading.Thread(target=lot.allocate, args=("car-2", results))
t1.start(); t2.start(); t1.join(); t2.join()

for r in sorted(results):
    print(r)
print(f"Spots given out: {sum('got the spot' in r for r in results)}")
$ python race_condition.py
car-1 got the spot
car-2 got the spot
Spots given out: 2

One spot, two cars, both “got” it. Both threads read self.occupied as False before either one has a chance to set it to True — the sleep(0.05) guarantees this interleaving happens every time this runs, but the same bug can occur without any sleep at all, just less predictably, whenever two threads check-then-act on shared state without coordination.

The fix: a lock around check-and-act together

import threading
import time

class SafeParkingLot:
    def __init__(self):
        self.occupied = False
        self._lock = threading.Lock()

    def allocate(self, vehicle_id, results):
        with self._lock:
            if not self.occupied:
                time.sleep(0.05)
                self.occupied = True
                results.append(f"{vehicle_id} got the spot")
            else:
                results.append(f"{vehicle_id} found the spot full")

lot = SafeParkingLot()
results = []
t1 = threading.Thread(target=lot.allocate, args=("car-1", results))
t2 = threading.Thread(target=lot.allocate, args=("car-2", results))
t1.start(); t2.start(); t1.join(); t2.join()

for r in sorted(results):
    print(r)
print(f"Spots given out: {sum('got the spot' in r for r in results)}")
$ python fixed_lock.py
car-1 got the spot
car-2 found the spot full
Spots given out: 1

Only one thread can be inside the with self._lock: block at once — whichever thread acquires it first runs the entire check-sleep-write sequence atomically with respect to the other thread, which has to wait at the with statement until the lock is released.

Practice

1. Confirm the bug gets worse with three threads instead of two.
import threading, time

class UnsafeParkingLot:
    def __init__(self):
        self.occupied = False
    def allocate(self, vehicle_id, results):
        if not self.occupied:
            time.sleep(0.05)
            self.occupied = True
            results.append(f"{vehicle_id} got the spot")
        else:
            results.append(f"{vehicle_id} found the spot full")

lot = UnsafeParkingLot()
results = []
threads = [threading.Thread(target=lot.allocate, args=(f"car-{i}", results)) for i in range(1, 4)]
for t in threads: t.start()
for t in threads: t.join()
for r in sorted(results):
    print(r)
print(f"Spots given out: {sum('got the spot' in r for r in results)}")
$ python three_thread_race.py
car-1 got the spot
car-2 got the spot
car-3 got the spot
Spots given out: 3

All three threads pass the check before any of them finishes the write — the bug scales with however many threads race, not just two.

2. Confirm the lock still protects all three threads correctly.
import threading, time

class SafeParkingLot:
    def __init__(self):
        self.occupied = False
        self._lock = threading.Lock()
    def allocate(self, vehicle_id, results):
        with self._lock:
            if not self.occupied:
                time.sleep(0.05)
                self.occupied = True
                results.append(f"{vehicle_id} got the spot")
            else:
                results.append(f"{vehicle_id} found the spot full")

lot = SafeParkingLot()
results = []
threads = [threading.Thread(target=lot.allocate, args=(f"car-{i}", results)) for i in range(1, 4)]
for t in threads: t.start()
for t in threads: t.join()
for r in sorted(results):
    print(r)
print(f"Spots given out: {sum('got the spot' in r for r in results)}")
$ python three_thread_safe.py
car-1 got the spot
car-2 found the spot full
car-3 found the spot full
Spots given out: 1

Exactly one spot given out no matter how many threads compete — the lock serializes them all through the same check-sleep-write sequence, one at a time.

3. Extend the lock-protected lot to a capacity of 2 spots instead of 1.
import threading, time

class SafeParkingLot:
    def __init__(self, capacity):
        self.capacity = capacity
        self.occupied = 0
        self._lock = threading.Lock()
    def allocate(self, vehicle_id, results):
        with self._lock:
            if self.occupied < self.capacity:
                time.sleep(0.05)
                self.occupied += 1
                results.append(f"{vehicle_id} got a spot")
            else:
                results.append(f"{vehicle_id} found the lot full")

lot = SafeParkingLot(capacity=2)
results = []
threads = [threading.Thread(target=lot.allocate, args=(f"car-{i}", results)) for i in range(1, 4)]
for t in threads: t.start()
for t in threads: t.join()
for r in sorted(results):
    print(r)
print(f"Spots given out: {sum('got a spot' in r for r in results)}")
$ python capacity_two.py
car-1 got a spot
car-2 got a spot
car-3 found the lot full
Spots given out: 2

A boolean occupied became an integer counter compared against capacity, but the same lock pattern (guard the check-and-increment together) still holds it at exactly 2, never 3.

Next: the Observer pattern, solving a concrete problem — a stock ticker that has to notify multiple displays without knowing what they are.

Frequently Asked Questions

Why does time.sleep(0.05) appear between the check and the write — isn't that cheating?
It's making an already-real bug reliably reproducible, not fabricating one. The race exists with or without the sleep — real concurrent systems hit this exact bug under real load, just unpredictably. The sleep widens the window between 'check' and 'act' so the interleaving happens every single time this lesson runs, rather than occasionally under conditions this page can't control.
Why sort results before printing instead of printing as each thread finishes?
Thread scheduling order isn't guaranteed — running this script twice can print 'car-1' before 'car-2' or vice versa, even though the *bug* (both getting the spot) happens every time. Collecting results into a list and sorting before printing keeps this lesson's output deterministic and reproducible, which is a strict requirement here, without hiding or changing the actual race.
Does the Lock need to wrap the sleep() call too, or just the check and the write?
It needs to wrap all three: the check, the simulated work, and the write — that's the whole point. A lock only prevents *other threads* from entering the same locked block while one thread holds it; if the sleep were outside the `with self._lock:` block, a second thread could still slip in during it and recreate the exact race this lesson is fixing.
What's the difference between this Lock and a Semaphore for a multi-spot lot?
A `Lock` allows exactly one thread through at a time — right for the single-spot version here. A capacity-N lot (Practice #3) still uses a `Lock` around a counter compared against `capacity`, which works fine at this scale; `threading.Semaphore(capacity)` is the more idiomatic tool for 'allow up to N concurrent holders' once the counter-and-lock pattern gets more complex than this.