Coding Interview: Concurrency Patterns — Threads, Locks, Mutex, Semaphore, Deadlock, Producer-Consumer, Async/Await

Concurrency questions test your understanding of parallel execution, synchronization, and the subtle bugs that arise when multiple threads share state. These questions appear in coding interviews at Google, Amazon, and other companies, especially for senior backend and systems roles. This guide covers essential concurrency primitives, classic patterns, and common pitfalls — giving you the foundation to solve any concurrency problem in an interview.

Threads vs Processes vs Coroutines

Process: an independent execution unit with its own memory space. Processes do not share memory — communication requires IPC (pipes, sockets, shared memory). Creating a process is expensive (fork copies the memory space). Use for: isolation (crash in one process does not affect others), multi-core CPU-bound work in languages with a GIL (Python multiprocessing). Thread: a lightweight execution unit within a process. Threads share the same memory space. Communication is fast (read/write shared variables) but requires synchronization (locks, mutexes) to prevent data corruption. Creating a thread is cheaper than a process but still involves kernel overhead. Use for: concurrent I/O, parallelizing CPU-bound work (Java, Go, Rust). Coroutine (async/await): a user-space execution unit managed by the language runtime, not the OS kernel. Thousands of coroutines can run on a single thread. Coroutines yield cooperatively (not preempted by the OS). Use for: high-concurrency I/O workloads (web servers handling thousands of concurrent connections). Go goroutines, Python asyncio, JavaScript async/await, and Kotlin coroutines are all coroutine-based concurrency models.

Mutex, Semaphore, and Condition Variable

Mutex (mutual exclusion): allows only one thread to access a shared resource at a time. A thread acquires the mutex (lock), accesses the resource, and releases the mutex (unlock). If another thread tries to acquire a locked mutex, it blocks until the mutex is released. Use for: protecting shared state (incrementing a counter, modifying a shared data structure). Semaphore: a generalized mutex that allows up to N concurrent accesses. Initialize with count N. Each acquire decrements the count; each release increments it. When count reaches 0, subsequent acquires block. Use for: limiting concurrency (at most 10 database connections, at most 5 concurrent HTTP requests). A semaphore with N=1 is equivalent to a mutex. Condition variable: allows a thread to wait until a specific condition is met. Used with a mutex: the thread acquires the mutex, checks the condition, and if the condition is false, waits on the condition variable (releasing the mutex atomically). Another thread signals the condition variable when the condition becomes true, waking the waiting thread. Use for: producer-consumer (consumer waits until the buffer is non-empty), thread pool (worker waits until a task is available).

Deadlock: Causes and Prevention

Deadlock occurs when two or more threads are blocked forever, each waiting for a resource held by the other. Four necessary conditions (Coffman conditions): (1) Mutual exclusion — resources cannot be shared. (2) Hold and wait — a thread holds one resource while waiting for another. (3) No preemption — resources cannot be forcibly taken from a thread. (4) Circular wait — a cycle of threads each waiting for a resource held by the next. Classic example: Thread A holds Lock 1 and waits for Lock 2. Thread B holds Lock 2 and waits for Lock 1. Neither can proceed. Prevention strategies: (1) Lock ordering — always acquire locks in the same global order. If all threads acquire Lock 1 before Lock 2, circular wait is impossible. (2) Try-lock with timeout — use tryLock(timeout) instead of lock(). If the lock is not acquired within the timeout, release all held locks and retry. (3) Avoid holding multiple locks — redesign to minimize the number of locks held simultaneously. (4) Lock-free data structures — use atomic operations (compare-and-swap) instead of locks. Java ConcurrentHashMap, Go sync.Map, and lock-free queues avoid deadlock entirely. Detection: database systems detect deadlocks by finding cycles in the wait-for graph and aborting one transaction.

Producer-Consumer Pattern

The producer-consumer pattern decouples data production from consumption using a shared buffer. One or more producer threads generate data and place it in the buffer. One or more consumer threads take data from the buffer and process it. The buffer is bounded (fixed capacity) to prevent memory exhaustion. Synchronization: (1) The producer must wait if the buffer is full (backpressure). (2) The consumer must wait if the buffer is empty. (3) Buffer access must be thread-safe (mutex). Implementation with a condition variable: the producer acquires the mutex, checks if the buffer is full. If full, waits on the not_full condition variable. Otherwise, adds the item and signals not_empty. The consumer acquires the mutex, checks if the buffer is empty. If empty, waits on not_empty. Otherwise, removes the item and signals not_full. In practice: Java BlockingQueue (ArrayBlockingQueue, LinkedBlockingQueue), Go channels (buffered channels are bounded producer-consumer queues), and Python queue.Queue provide thread-safe producer-consumer implementations. Interview tip: when asked to implement a thread-safe bounded buffer, use a mutex + two condition variables (not_full, not_empty).

Reader-Writer Lock Pattern

A reader-writer lock allows concurrent reads but exclusive writes. Multiple threads can hold read locks simultaneously (reading shared state is safe when no one is writing). Only one thread can hold the write lock, and no readers are allowed during a write. Use case: a shared configuration object read frequently (1000 reads/sec) and updated rarely (1 write/min). A regular mutex would serialize all reads unnecessarily. A reader-writer lock allows all reads to proceed in parallel and only blocks when a write occurs. Implementation: Java ReadWriteLock, Go sync.RWMutex, Python threading.RWLock. Writer starvation: if readers continuously acquire read locks, a waiting writer may never get the write lock. Solution: writer-preference locks — once a writer is waiting, no new readers are admitted. Existing readers finish, the writer runs, then readers are admitted again. Read-copy-update (RCU): an alternative to reader-writer locks used in the Linux kernel. Readers access the current version without any lock. Writers create a new copy of the data, modify it, and atomically swap the pointer. Old copies are freed after all readers using them complete. RCU provides zero-overhead reads at the cost of more complex writes.

Common Concurrency Interview Questions

Classic problems to practice: (1) Print in order — three threads print “first”, “second”, “third” in order. Use semaphores or condition variables to enforce ordering. (2) Print FizzBuzz with threads — four threads (fizz, buzz, fizzbuzz, number) coordinate to print the FizzBuzz sequence. Each thread handles its specific numbers. (3) Dining philosophers — five philosophers share five forks. Each needs two forks to eat. Avoid deadlock and starvation. Solution: order the forks and always pick up the lower-numbered fork first. (4) Bounded blocking queue — implement put(item) and take() with blocking when full/empty. Use a mutex + two condition variables. (5) Rate limiter — implement a thread-safe token bucket rate limiter. Use atomic operations for the token count. (6) Read-write lock — implement a reader-writer lock from a mutex and condition variables. Track the number of active readers and whether a writer is active. For each problem: identify the shared state, the synchronization requirement (ordering, mutual exclusion, bounded concurrency), and choose the appropriate primitive (mutex, semaphore, condition variable, atomic).

Scroll to Top