CMD Guide
HomeConcurrencyConcurrency Problems

Problem 14 Advanced Synchronization in Multi-Buffered Master-Worker Thread Pools

The right primitive turns this into three lines

A master produces items and workers consume them through a bounded buffer: producers must block when it's full, consumers must block when it's empty. The clean classic uses two counting semaphoresempty (slots free, starts at capacity) and full (items ready, starts at 0) — plus a mutex to protect the buffer structure itself. The earlier version mixed prose about "condition variables" with semaphore code, left a semaphore that was never acquired, and busy-spun a worker — all symptoms of not letting one primitive own the blocking.

Producer acquires an empty slot then releases a full slot; consumer acquires a full slot then releases an empty slot; a mutex guards the buffer
Producer acquires an empty slot then releases a full slot; consumer acquires a full slot then releases an empty slot; a mutex guards the buffer

Correct Java

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;

class BoundedBuffer<T> {
    private final Queue<T> buf = new ArrayDeque<>();
    private final Semaphore empty, full;
    private final ReentrantLock mutex = new ReentrantLock();
    BoundedBuffer(int capacity) { empty = new Semaphore(capacity); full = new Semaphore(0); }

    public void put(T x) throws InterruptedException {
        empty.acquire();                 // blocks if buffer full
        mutex.lock();
        try { buf.add(x); } finally { mutex.unlock(); }
        full.release();                  // one more item to consume
    }
    public T take() throws InterruptedException {
        full.acquire();                  // blocks if buffer empty
        mutex.lock();
        T x;
        try { x = buf.poll(); } finally { mutex.unlock(); }
        empty.release();                 // one more free slot
        return x;
    }
}

Acquire the counting semaphore before the mutex, never the reverse — holding the mutex while blocking on a full/empty semaphore would deadlock the whole buffer.

Correct Go — the buffer IS a channel

Go folds all three objects into one: a buffered channel already blocks the sender when full and the receiver when empty, with the synchronization built in. This is the idiomatic answer and why Go concurrency is so terse here.

buf := make(chan T, capacity)   // bounded buffer
// producer:  buf <- x          // blocks when full
// consumer:  x := <-buf        // blocks when empty
// shut down: close(buf); for x := range buf { ... }   // drains then ends

Pitfalls

Takeaways


Re-authored for correctness for this guide (the prior version had prose/code mismatch, a dead semaphore, and a busy-spin). Classic producer-consumer (Dijkstra). See also: Semaphore, Condition Variables, Mutex Lock.

Sizing the buffer: the latency–memory–throughput triangle

The capacity C is not a free parameter — it trades three things against each other. An item can sit behind up to C−1 others, so when the buffer runs near-full the time an item waits before a worker picks it up is roughly C / μ, where μ is the aggregate consumer rate; by Little's law the average occupancy L = λ·W ties arrival rate, wait, and depth together.

Concrete break-even: if consumers drain at μ and producers can burst at λpeak for a duration t, size C ≥ (λpeak − μ)·t to ride out the burst without ever blocking (that is exactly the net items accumulated during the burst). Below that the producer blocks for the overflow; above it you are only buying latency and memory for no throughput gain.

When producers outrun workers: block, drop, or OOM

A bounded buffer is backpressure by construction: when it is full, put() blocks on the empty semaphore, and that stall propagates upstream until the producer slows to the consumer's pace. Memory stays capped at C. The alternatives make the opposite trade:

Decision: block when the producer can afford to slow down and you need zero loss (batch / ETL); reject or drop when the producer must stay live and the data is lossy-tolerant (metrics, logs); never unbounded unless the consumer provably never falls behind.

Shutting down N workers cleanly: the poison pill

Closing a shared buffer to many workers has a trap: enqueue one sentinel and only one worker sees it — the other N−1 block forever on the empty buffer. So the producer enqueues one poison per worker after the last real item:

static final Object POISON = new Object();

// producer, after the last real item:
for (int w = 0; w < N_WORKERS; w++) buffer.put(POISON);

// each worker:
while (true) {
    T x = buffer.take();
    if (x == POISON) break;      // one poison per worker → each stops exactly once
    process(x);
}

If the producer does not know N_WORKERS, use the cascade: a worker that takes the poison re-inserts it before exiting (buffer.put(POISON); break;), so it propagates worker to worker. In Go you get this for free — close(jobs) after the producers finish, and every range over the channel exits cleanly; the channel close is the multi-worker poison pill.

🤖 Don't fully get this? Learn it with Claude

Stuck on Problem 14 Advanced Synchronization in Multi-Buffered Master-Worker Thread Pools? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🪜 Hint ladder (no spoilers)

Progressively stronger hints — you still solve it.

I'm working on the problem **Problem 14 Advanced Synchronization in Multi-Buffered Master-Worker Thread Pools** (Concurrency). Give me a HINT LADDER: start with the tiniest nudge, then wait. Only reveal the next, stronger hint when I ask. Do NOT show the full solution unless I type 'show solution'. Keep me doing the thinking. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🎨 Explain the approach visually

See the technique, not just code.

Explain the optimal approach to **Problem 14 Advanced Synchronization in Multi-Buffered Master-Worker Thread Pools** with a VISUAL walkthrough: trace it on a small concrete example using ASCII art / a step-by-step diagram, narrate what changes each step, then give time & space complexity with a one-line derivation. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🔍 Review my solution

Catch bugs, edge cases, sub-optimality.

I'll paste my solution to **Problem 14 Advanced Synchronization in Multi-Buffered Master-Worker Thread Pools**. Review it for correctness, missed edge cases, and time/space complexity, then coach me toward the optimal — don't just rewrite it. Ask me to paste my code now. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🔁 Drill the pattern

Lock in recognition with look-alikes.

Give me 2 problems that use the SAME underlying pattern as **Problem 14 Advanced Synchronization in Multi-Buffered Master-Worker Thread Pools**. For each, let me attempt first, then review my answer and name the trigger signal that reveals the pattern. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes