Locks, Thread-Safe Singletons & Producer-Consumer
You'll learn to
- -Use locks and read-write locks correctly, and implement a thread-safe Singleton without over-synchronizing
- -Implement a bounded blocking queue for a producer-consumer relationship
Locks are the most direct fix for the check-then-act race from the previous chapter: wrap the critical section - the check and the act together - in a lock so only one thread can execute it at a time.
Fixing the Parking Lot Race With a Lock
import threading
class ParkingLot:
def __init__(self, total_spots: int):
self.available_spots = total_spots
self._lock = threading.Lock()
def assign_spot(self) -> bool:
with self._lock: # only one thread inside at a time
if self.available_spots > 0:
self.available_spots -= 1
return True
return FalseCoarse-Grained vs. Fine-Grained Locking
A single lock around the entire ParkingLot (coarse-grained) is simple and definitely correct, but it serializes every spot assignment across the whole lot, even in different, unrelated sections. Fine-grained locking - a separate lock per level or per section of the lot - lets unrelated sections proceed in parallel, at the cost of more complexity and a new risk: if a request ever needs to hold two locks at once (moving a car between sections), inconsistent lock-acquisition order across different code paths can cause deadlock. The practical interview guidance: default to coarse-grained locking for correctness and simplicity, and only justify fine-grained locking with a concrete contention argument ("Level 1 and Level 2 assignment happening on separate locks matters if we expect thousands of simultaneous requests split across levels").
Read-Write Locks: Optimizing for the Read-Heavy Case
When a resource is read far more often than it is written (checking available_spots is far more common than actually assigning one), a plain lock still forces reads to serialize against each other unnecessarily, even though two simultaneous reads cannot conflict. A read-write lock allows any number of concurrent readers, but grants exclusive access to a single writer - so read-heavy workloads scale much better without sacrificing the writer's exclusivity guarantee.
Thread-Safe Singleton, Revisited
The double-checked locking Singleton from the Creational Patterns module is the textbook example of applying a lock surgically rather than everywhere: locking only the narrow window where the instance might not yet exist, and skipping the lock entirely (via the outer check) once initialization has already happened - avoiding the cost of a lock on every single subsequent access.
Producer-Consumer: A Bounded Blocking Queue
Producer-consumer is one of the most reusable concurrency shapes in LLD: one or more producer threads generate work items, one or more consumer threads process them, and a shared, bounded queue sits between them - blocking producers when the queue is full, and blocking consumers when the queue is empty, rather than spinning or dropping data.
import threading
from collections import deque
class BoundedBlockingQueue:
def __init__(self, capacity: int):
self._queue = deque()
self._capacity = capacity
self._lock = threading.Lock()
self._not_full = threading.Condition(self._lock)
self._not_empty = threading.Condition(self._lock)
def put(self, item) -> None:
with self._not_full:
while len(self._queue) >= self._capacity:
self._not_full.wait() # block until there's room
self._queue.append(item)
self._not_empty.notify()
def take(self):
with self._not_empty:
while not self._queue:
self._not_empty.wait() # block until there's an item
item = self._queue.popleft()
self._not_full.notify()
return itemThis exact bounded-queue shape is worth recognizing on sight: it is the same mechanism behind thread pools, message queues, and rate limiters with a "burst capacity" - producers and consumers proceeding at different speeds, coordinated through one shared, bounded buffer.
Interview Signal is part of Pro
See a real weak answer next to a real strong one for this exact topic.
Quiz is part of Pro
Test what you just read with a short quiz, and bank the XP.