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
| Primitive | Guarantees | Use for |
|---|---|---|
| Mutex / lock | one thread in the critical section | protect shared mutable state |
| Semaphore | at most N permits | bound a resource pool / signal counts |
| Condition variable | wait until a predicate holds, then wake | "block until the buffer isn't full/empty" |
| Atomic | lock-free read-modify-write | counters, 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.
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 — 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++ — 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;
}
};
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
PROBLEMThree methods first(), second(), third() are called on separate threads in arbitrary order. Guarantee they execute in the order first → second → third.
- 1
Name the hazard
“What goes wrong if I do nothing?”
- 2
Pick the primitive
“Which tool expresses 'wait until X has happened'?”
unlocks after the stage above - 3
Wire the chain
“How do the permits flow?”
unlocks after the stage above
// 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++ — 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
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- Print in OrderEasy
Two gates (semaphores) to force a happens-before chain.
- Print FooBar AlternatelyMedium
Two threads ping-ponging via two semaphores.
- Building H2OMedium
Barrier-style grouping: 2 H + 1 O before release.
Core — the classics
the named problems every interviewer knows- Print Zero Even OddMedium
Three-way coordination with condition variables.
- Fizz Buzz MultithreadedMedium
Four threads gated on a shared counter's state.
- The Dining PhilosophersMedium
Break circular wait via lock ordering — the deadlock lesson.
Stretch — shared structures
build a concurrent componentThread pool + concurrent visited-set — a tiny crawler.
The producer-consumer buffer as a reusable class.
Check yourself
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
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- Print in OrderEasy
Semaphore signaling — thread A signals thread B to proceed.
- Print FooBar AlternatelyMedium
Two semaphores alternating — the simplest producer-consumer.
Core
the bounded-buffer and barrier problems- Print Zero Even OddMedium
Three-thread coordination — lock + condition variables.
- Building H2OMedium
Barrier pattern — wait until 2 H + 1 O are ready before releasing.
- The Dining PhilosophersMedium
Deadlock avoidance via lock ordering — the Coffman conditions in practice.
Stretch
bounded readers-writers and traffic controlMutex-controlled state machine — concurrent access with a single lock.