Concurrency
LeetCode tag: Concurrency (~20–30 problems). Depth tier: < 50 problems — core concepts, main patterns, key primitives.
Core Concepts
Thread — an independent sequence of execution sharing memory with other threads in the same process. Multiple threads can run the same function simultaneously, causing races if shared state is not protected.
Race condition — two or more threads read-modify-write shared state without coordination, producing results that depend on scheduling order. The source of almost every concurrency bug.
Critical section — a code region that must execute atomically with respect to other threads. Correctness requires that at most one thread (or a bounded number) is inside it at any time.
Atomicity — an operation that completes entirely before any other thread observes its effect. Hardware provides atomic reads/writes for native word sizes; everything else needs explicit synchronization.
Mutual exclusion (mutex / lock) — a binary synchronization object; at most one thread holds it at a time. A thread that cannot acquire it blocks until the holder releases it.
Semaphore — an integer counter with two operations: acquire (decrement; block if count reaches 0) and release (increment; unblock one waiting thread). A mutex is a semaphore initialized to 1. Counting semaphores coordinate bounded resources or signal between threads.
Condition variable — a wait/notify mechanism always paired with a lock. wait() atomically releases the lock and suspends the thread; notify() / notify_all() wakes waiting threads, which must reacquire the lock before returning.
Barrier — a rendezvous point: all N threads must arrive before any may proceed. Used when the next phase requires the previous phase to complete across all threads.
Event — a boolean flag with blocking semantics. wait() blocks until set() is called; clear() resets it. One-shot signalling with no counting.
Monitor — the combination of a lock + one or more condition variables protecting a shared object. Python's threading.Condition is a monitor.
Deadlock — a cycle of threads each waiting for a resource held by the next. Requires: mutual exclusion, hold-and-wait, no preemption, circular wait. Breaking any one condition prevents it.
Starvation — a thread is perpetually denied a resource because others are always preferred. Distinct from deadlock: the system makes progress, but unfairly.
Spurious wakeup — a thread woken from condition.wait() without a corresponding notify. Always recheck the predicate in a loop after waking.
Structural Invariants
| Primitive | Invariant | What breaks if violated |
|---|---|---|
| Mutex (Lock) | count ∈ {0, 1}; only the acquiring thread may release | Double-release corrupts the count; releasing from a different thread is undefined |
| Semaphore | count ≥ 0 at all times | Releasing beyond initial value overshoots; extra threads enter the critical region |
| Condition variable | must hold the associated lock before calling wait / notify | wait without the lock is a data race on the internal waitlist; notify may be lost |
| Barrier | reuse counter resets only after all N threads have passed | Partial reset lets faster threads lap slower ones, causing them to skip a phase |
Types / Variants
| Primitive | What it is | Use over alternatives when |
|---|---|---|
threading.Lock | Binary mutex; non-reentrant | Simple mutual exclusion; one thread at a time |
threading.RLock | Reentrant lock; same thread may acquire multiple times | A function holding a lock calls another function that also locks the same object |
threading.Semaphore | Counting semaphore | Limiting concurrency to N > 1 threads, or signalling between threads |
threading.BoundedSemaphore | Semaphore that raises on over-release | When you want an assertion that release is never called more times than acquire |
threading.Condition | Lock + wait/notify queue | Waiting for a predicate on shared state (not just access); producer-consumer |
threading.Barrier | N-thread rendezvous | All threads must finish phase K before any starts phase K+1 |
threading.Event | One-shot boolean signal | "Thread B must not start until thread A has finished setup" — fire-once signalling |
Key Algorithms
Semaphore-Based Ordering
Enforce that method B executes after method A completes by initializing a semaphore to 0, releasing it at the end of A, and acquiring it at the start of B. Generalizes to chains.
- Time: O(1) per operation. Space: O(1).
sem = Semaphore(0)
# in A: do work, then sem.release()
# in B: sem.acquire(), then do work
Condition Variable with Predicate Loop
Wait until a shared condition becomes true. Always wrap wait() in a while loop (not if) to handle spurious wakeups and the window between notify and reacquiring the lock.
- Time: O(1) amortized. Space: O(1).
with cond:
while not predicate():
cond.wait()
# safe to proceed
Barrier Synchronization
Use threading.Barrier(n) when n threads must all reach a checkpoint before any continues. Each thread calls barrier.wait(); the (n)th arrival unblocks all.
- Time: O(1) per thread. Space: O(n) for the waitlist.
Lock-Protected Counter / State Machine
Protect a shared integer (or state enum) with a lock. Threads acquire the lock, check/update the value, then release. Used for turn-based alternation.
with lock:
shared_state += 1
Event-Based One-Shot Signal
Initialize threading.Event(). The blocking thread calls event.wait(); the signalling thread calls event.set(). Unlike semaphores, set() on an already-set event is idempotent and all waiters unblock.
Problem Taxonomy
By Problem Category
| Category | What it asks | Key technique |
|---|---|---|
| Ordered execution | Methods A, B, C must run in strict sequence across threads | Chained semaphores (0-initialized, release at end of each phase) |
| Alternating threads | Two threads must interleave their outputs in a fixed pattern | Two semaphores, each initialized to grant one thread access at a time |
| N-thread round-robin | N threads take turns in cyclic order | N semaphores in a ring, or a lock + condition + turn counter |
| Bounded producer-consumer | One or more producers fill a buffer; consumers drain it | Two semaphores (empty-slots, filled-slots) + a mutex for the buffer |
| FizzBuzz / categorized output | Threads specialize by output type; must collectively produce correct order | Condition variable checking divisibility on a shared counter |
| Barrier / phase synchronization | All threads complete phase K before phase K+1 | threading.Barrier or manual counter under a condition variable |
| Rate limiting / throttling | At most K threads execute a section concurrently | Semaphore(k) as a concurrency limiter |
Problem Signal → Technique
| Signal in problem statement | Likely technique |
|---|---|
| "second call must happen after first" | Semaphore initialized to 0 |
| "in the same order" / "print in order" | Chained semaphores |
| "alternate" / "take turns" | Paired semaphores or condition + turn variable |
| "at most N threads at a time" | Counting semaphore initialized to N |
| "zero-barrier" / "all threads ready" | threading.Barrier |
| "notify when done" / "wait until X" | threading.Event |
| "producer … consumer" | Two semaphores + mutex |
| "fizz" / "buzz" / multiple categories on one counter | Condition variable + shared counter |
Common Patterns
| Pattern | When it applies | Mechanism |
|---|---|---|
| Semaphore chain | Enforce strict A→B→C ordering | Each step releases the next step's semaphore; all start at 0 except the first (or the first is triggered externally) |
| Ping-pong semaphores | Two threads must strictly alternate | sem_a = Semaphore(1), sem_b = Semaphore(0); thread A acquires sem_a then releases sem_b; thread B does the reverse |
| Condition + counter | Multiple threads each responsible for one output category | Shared counter under a condition variable; each thread loops: acquire lock, check if it's their turn, if not wait, else output and increment, then notify_all |
| Concurrency limiter | Restrict parallelism to K | sem = Semaphore(k); each thread acquires before entering, releases on exit |
| Barrier phasing | Pipeline stages where each stage needs all results from previous | barrier.wait() at the end of each stage; all threads block until the last arrives |
| Event gate | One-time go-signal from an initializer thread | event.wait() in worker threads; initializer calls event.set() once ready |
Edge Cases & Pitfalls
-
Releasing a semaphore too many times. If
release()is called more times thanacquire(), the count exceeds its logical maximum and extra threads enter the critical section. UseBoundedSemaphoreto catch this. -
Using
ifinstead ofwhilefor condition variables. Betweennotifyand the waiter reacquiring the lock, another thread may change the predicate. Alwayswhile not predicate(): cond.wait(). -
Spurious wakeups.
condition.wait()may return without anotify. Thewhile-loop fix covers this simultaneously with the previous pitfall. -
Deadlock from acquisition order. If thread A holds lock-1 and waits for lock-2 while thread B holds lock-2 and waits for lock-1, neither can proceed. Fix: always acquire multiple locks in the same global order across all threads.
-
Forgetting to notify after state change. A thread updates shared state under a condition variable but omits
notify_all(). Waiting threads sleep indefinitely even though the predicate is now true. -
Calling
condition.wait()without holding the lock. In Python this raisesRuntimeError. Always use thewith cond:context manager. -
Reusing a one-shot Event.
Event.set()is permanent untilEvent.clear()is called. If the event is meant to signal once, do not callclear(); if it must reset per round, callclear()before the next cycle while under a lock. -
Thread starvation with non-fair semaphores. Python's semaphore uses a FIFO waitlist, so starvation is unlikely in practice, but logical starvation can occur if one thread acquires and releases in a tight loop, always re-acquiring before others wake.
-
Not joining threads before checking results. Reading shared state before calling
thread.join()is a race: the thread may not have written its result yet.
Implementation Notes
- Python's
threading.Lockis non-reentrant: a thread that callsacquire()twice without releasing will deadlock with itself. UseRLockif re-entrant locking is needed. threading.Semaphorein Python is internally protected by aCondition, making it thread-safe, butrelease()does not enforce an upper bound — useBoundedSemaphorefor that.- The GIL does not protect custom data structures. Even though only one Python bytecode executes at a time, compound read-modify-write operations are not atomic; still use locks.
condition.notify()wakes exactly one waiter;condition.notify_all()wakes all. Prefernotify_all()when multiple threads might satisfy the predicate after a state change, to avoid missing a thread.- LeetCode concurrency problems are run in a multi-threaded harness; the solution class is instantiated once and methods are called from different threads concurrently. Initialize all synchronization objects in
__init__. threading.BarrierraisesBrokenBarrierErrorif a thread times out or ifabort()is called. In interview settings, assume no timeout.
Cross-Topic Relations
| Related topic | Connection |
|---|---|
| Operating Systems (Semaphores, Monitors) | Concurrency primitives originate in OS theory; the LeetCode problems are direct implementations of textbook synchronization exercises |
| Producer-Consumer (classic CS problem) | A recurring pattern; the two-semaphore + mutex solution maps directly to several LeetCode problems |
| State Machines | Turn-based alternation problems are modeled as a shared state variable with transitions guarded by locks |
| Queue / Deque | Bounded producer-consumer problems often use a queue as the shared buffer; the synchronization wraps queue operations |
Interview Reference
Must-Know Problems
Ordered Execution
- Print in Order (LC 1114) — three methods, enforce A→B→C; classic semaphore chain
- Print FooBar Alternately (LC 1115) — two threads alternating output; ping-pong semaphores
Categorized Output
- Print Zero Even Odd (LC 1116) — three threads, one counter, each thread handles one category; condition variable + counter
- Fizz Buzz Multithreaded (LC 1195) — four threads, divisibility-based categories; condition variable pattern
Producer-Consumer
- Building H2O (LC 1117) — two resource types must combine in fixed ratio; two semaphores + barrier
- The Dining Philosophers (LC 1226) — classic deadlock avoidance; ordered lock acquisition or a waiter semaphore
Barrier / Phasing
- Web Crawler Multithreaded (LC 1242) — crawl level-by-level; thread pool + barrier or concurrent queue
Clarification Questions to Ask
- How many threads will call each method — exactly as specified, or up to N?
- Is the method called at most once per thread, or can the same thread call it multiple times?
- Is output order guaranteed to be observed by the grader, or just that the calls complete?
- Are there timing guarantees, or can threads be delayed arbitrarily?
Common Interview Mistakes
- Initializing a semaphore to 1 when it should be 0 for ordering (1 means "one unit available now", 0 means "wait for a signal").
- Using a bare
Lockwhen aConditionis needed — a lock alone cannot express "wait until counter == 3". - Not calling
notify/notify_allafter updating the shared state, leaving waiting threads permanently blocked. - Using
ifinstead ofwhilearoundcondition.wait(), making the solution fragile under spurious wakeups. - Releasing locks in the wrong order or forgetting
releaseon an exception path — usewithstatements to guarantee release. - Deadlocking the Dining Philosophers by having all philosophers pick up the left fork simultaneously — fix by making one philosopher pick up right-first.
Typical Follow-Up Escalations
- "Now allow up to K threads to execute concurrently" → replace the binary semaphore with
Semaphore(k). - "Now make it work for N threads instead of 2" → generalize ping-pong to a semaphore ring of N elements.
- "How would you detect deadlock at runtime?" → cycle detection on a resource-allocation graph; or timeout + retry with backoff.
- "How does this change if we use processes instead of threads?" →
multiprocessing.Semaphoreuses OS-level shared memory; the GIL no longer applies;Value/Managerneeded for shared state. - "What if
releasecan be called from a different thread thanacquire?" →Lockis not suited (owner-based); use aSemaphore, which has no ownership concept.
Read Next
Start reading to get personalized recommendations
Explore Unread
Great job! You've read all available articles