Concurrency Coding Problems

The multithreading round that isn't really DSA: locks, semaphores and condition variables, then the classics — producer-consumer, readers-writers, dining philosophers — plus the deadlock conditions and how to avoid them.

concurrencythreadssynchronizationdeadlock

A different kind of coding round

Some companies (Google, Microsoft, Nvidia, Bloomberg, and most HFT firms) run a concurrency coding round: not "find the subarray" but "make these threads cooperate correctly." It tests whether you reach for the right primitive and reason about interleavings — see also the concurrency fundamentals.

💡 Visualizing the primitives:

  • Mutex (Bathroom Key): Imagine a busy coffee shop with a single bathroom. To use it, you must get the key (the lock/mutex). Only one person can hold the key at a time. Anyone else must wait until the key is returned.
  • Semaphore (Parking Lot): Imagine a parking lot with a capacity of 10 cars. When a car enters, the counter decrements. If the lot is full (counter is 0), cars must queue at the gate. When a car leaves, the counter increments and the gate lets the next car in.
  • Condition Variable (Restaurant Buzzer): Instead of standing at the counter repeatedly asking the chef "Is my food ready yet?" (busy-spinning / polling), you are given a buzzer. You sit down and wait silently. When the chef notifies the buzzer (condition variable), you wake up and walk to the counter to collect your food.

The primitives

PrimitiveGuaranteesUse for
Mutex / lockone thread in the critical sectionprotect shared mutable state
Semaphoreat most N permitsbound a resource pool / signal counts
Condition variablewait until a predicate holds, then wake"block until the buffer isn't full/empty"
Atomiclock-free read-modify-writecounters, flags, CAS loops

The recurring shape: a lock for mutual exclusion, plus a condition variable to wait for a state instead of busy-spinning.

Producer–consumer (bounded buffer)

The canonical problem and the template for most of the others: producers add to a fixed buffer, consumers remove, and each blocks when it can't proceed.

Python
import threading
from collections import deque

class BoundedBuffer:
    def __init__(self, capacity):
        self.buf = deque()
        self.cap = capacity
        self.lock = threading.Lock()
        self.not_full = threading.Condition(self.lock)
        self.not_empty = threading.Condition(self.lock)

    def put(self, item):
        with self.not_full:
            while len(self.buf) == self.cap:   # while, not if — guard against spurious wakeups
                self.not_full.wait()
            self.buf.append(item)
            self.not_empty.notify()            # wake one waiting consumer

    def get(self):
        with self.not_empty:
            while not self.buf:
                self.not_empty.wait()
            item = self.buf.popleft()
            self.not_full.notify()
            return item
Java
// Java — Producer-Consumer (Bounded Buffer)
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class BoundedBuffer<T> {
    private final Queue<T> queue = new LinkedList<>();
    private final int capacity;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public BoundedBuffer(int capacity) {
        this.capacity = capacity;
    }

    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) { // while loop guards against spurious wakeups
                notFull.await();
            }
            queue.add(item);
            notEmpty.signal(); // wake one waiting consumer
        } finally {
            lock.unlock();
        }
    }

    public T get() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await();
            }
            T item = queue.poll();
            notFull.signal(); // wake one waiting producer
            return item;
        } finally {
            lock.unlock();
        }
    }
}
C++
// C++ — Producer-Consumer (Bounded Buffer)
#include <queue>
#include <mutex>
#include <condition_variable>

template <typename T>
class BoundedBuffer {
private:
    std::queue<T> queue;
    size_t capacity;
    std::mutex mtx;
    std::condition_variable not_full;
    std::condition_variable not_empty;

public:
    BoundedBuffer(size_t cap) : capacity(cap) {}

    void put(T item) {
        std::unique_lock<std::mutex> lock(mtx);
        // while loop guards against spurious wakeups
        not_full.wait(lock, [this]() { return queue.size() < capacity; });
        queue.push(item);
        not_empty.notify_one();
    }

    T get() {
        std::unique_lock<std::mutex> lock(mtx);
        not_empty.wait(lock, [this]() { return !queue.empty(); });
        T item = queue.front();
        queue.pop();
        not_full.notify_one();
        return item;
    }
};
Always wait in a while loop, never an if

After wait() returns, the condition you waited for may already be false again (another thread raced in, or a spurious wakeup). Re-check the predicate in a while loop before proceeding. if here is one of the most common — and hardest to reproduce — concurrency bugs.

Think it through like the interview

Think it through: Print in Order / Foo-BarMedium — LeetCode 1114 / 11150/3 stages

PROBLEMThree methods first(), second(), third() are called on separate threads in arbitrary order. Guarantee they execute in the order first → second → third.

  1. 1

    Name the hazard

    What goes wrong if I do nothing?

  2. 2

    Pick the primitive

    Which tool expresses 'wait until X has happened'?

    unlocks after the stage above
  3. 3

    Wire the chain

    How do the permits flow?

    unlocks after the stage above
Java
// Java — Print in Order using Semaphores
import java.util.concurrent.Semaphore;

class Foo {
    private final Semaphore s2 = new Semaphore(0);
    private final Semaphore s3 = new Semaphore(0);

    public Foo() {}

    public void first(Runnable printFirst) throws InterruptedException {
        printFirst.run();
        s2.release();
    }

    public void second(Runnable printSecond) throws InterruptedException {
        s2.acquire();
        printSecond.run();
        s3.release();
    }

    public void third(Runnable printThird) throws InterruptedException {
        s3.acquire();
        printThird.run();
    }
}
C++
// C++ — Print in Order using Condition Variables
#include <mutex>
#include <condition_variable>
#include <functional>

class Foo {
private:
    std::mutex mtx;
    std::condition_variable cv;
    int step = 1;

public:
    Foo() {}

    void first(std::function<void()> printFirst) {
        std::unique_lock<std::mutex> lock(mtx);
        printFirst();
        step = 2;
        cv.notify_all();
    }

    void second(std::function<void()> printSecond) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [this]() { return step == 2; });
        printSecond();
        step = 3;
        cv.notify_all();
    }

    void third(std::function<void()> printThird) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [this]() { return step == 3; });
        printThird();
    }
};

Deadlock — the four conditions

A deadlock needs all four (Coffman conditions): mutual exclusion, hold-and-wait, no preemption, and circular wait. Break any one and deadlock can't happen. The practical fix interviewers want: impose a global lock ordering (always acquire locks in the same order) to break circular wait — the standard answer to dining philosophers (each philosopher grabs the lower-numbered fork first).

Practice — climb the ladder

Practice ladder: Concurrency0/8 solved

Climb in order — every rung assumes the one above it. Solve on LeetCode, then tick it here; progress is saved on this device.

Warm-up — ordering & signaling

semaphores and condition variables
  1. Two gates (semaphores) to force a happens-before chain.

  2. Two threads ping-ponging via two semaphores.

  3. Barrier-style grouping: 2 H + 1 O before release.

Core — the classics

the named problems every interviewer knows
  1. Three-way coordination with condition variables.

  2. Four threads gated on a shared counter's state.

  3. Break circular wait via lock ordering — the deadlock lesson.

Stretch — shared structures

build a concurrent component
  1. Thread pool + concurrent visited-set — a tiny crawler.

  2. The producer-consumer buffer as a reusable class.

Check yourself

Check yourself0/3 answered

1. Why must you check the condition in a while loop, rather than an if statement, after calling wait() on a condition variable?

2. Which of the following describes how lock ordering prevents deadlocks?

3. What is the key difference between a Mutex and a Semaphore?

Practice — climb the ladder

Practice ladder: Concurrency Coding0/6 solved

Climb in order — every rung assumes the one above it. Solve on LeetCode, then tick it here; progress is saved on this device.

Foundational

single-primitive problems — understand the lock/condition variable pattern
  1. Semaphore signaling — thread A signals thread B to proceed.

  2. Two semaphores alternating — the simplest producer-consumer.

Core

the bounded-buffer and barrier problems
  1. Three-thread coordination — lock + condition variables.

  2. Barrier pattern — wait until 2 H + 1 O are ready before releasing.

  3. Deadlock avoidance via lock ordering — the Coffman conditions in practice.

Stretch

bounded readers-writers and traffic control
  1. Mutex-controlled state machine — concurrent access with a single lock.

Visualize producer–consumer thread synchronization

Unknown visualizer: producer-consumer