Concurrency-Safe Patterns
You'll learn to
- -Adapt Builder and Object Pool for safe use under concurrent access
- -Explain the trade-off between coarse-grained locking (simple, slower) and fine-grained locking (faster, easy to get wrong)
Most of the patterns covered earlier in this course are inherently safe under concurrency because they produce immutable results or stateless objects. A few, though, need explicit thought when multiple threads use them at once - this chapter covers the two that come up most often as interview follow-ups.
Builder Under Concurrency: Usually a Non-Issue, With One Exception
A Builder instance used entirely within a single thread (construct, configure, build(), discard) needs no special handling - it is local, mutable state that never crosses a thread boundary. The exception is a shared Builder instance reused across threads, which reintroduces exactly the race conditions from earlier chapters: two threads calling .add_topping() on the same builder can interleave unpredictably. The fix is almost always simpler than adding locks: do not share Builder instances across threads. Each thread constructs its own Builder, and only the final, immutable result of build() is ever shared.
Object Pool: Reusing Expensive Objects Safely
Object Pool maintains a set of initialized, reusable objects (database connections, thread instances) rather than constructing and destroying them on every use, since construction is expensive. Under concurrency, the pool itself is a shared, mutable resource - the exact shape flagged in the "why concurrency matters" chapter - so acquiring and releasing objects from the pool needs to be thread-safe.
import threading
from collections import deque
class ConnectionPool:
def __init__(self, size: int):
self._available = deque(DatabaseConnection() for _ in range(size))
self._lock = threading.Lock()
self._connection_released = threading.Condition(self._lock)
def acquire(self) -> "DatabaseConnection":
with self._connection_released:
while not self._available:
self._connection_released.wait() # block until one is returned
return self._available.popleft()
def release(self, conn: "DatabaseConnection") -> None:
with self._connection_released:
self._available.append(conn)
self._connection_released.notify() # wake a waiting acquirerNotice this is the same bounded-blocking-queue mechanism from the previous chapter, applied to a pool of reusable objects instead of a stream of work items - a strong sign of how reusable these underlying concurrency primitives are once you recognize the shape.
A common bug in object pool implementations: forgetting to release a connection back to the pool on an exception path. A try/finally (or a context manager, in Python) around acquire/release is worth calling out explicitly - a leaked connection permanently shrinks the pool.
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.