Free preview

Why this matters: the pool's implementation is a direct transcription of lesson 02's rules, and that's the point — an interviewer watching you code is checking that the while loops, the lock scopes, and the catch-everything block land exactly where your design said they would. Drift between the words and the code is what gets probed.

The bounded queue

c
struct task_item { fn task; args a; struct handle *h; }; struct bounded_queue { struct task_item items[BOUND]; int head, count; // ring storage, plain ints: // ALL access is under mu mutex mu; condvar not_empty; // workers wait here condvar not_full; // (blocking-submit policy waits here) }; void q_push(struct bounded_queue *q, struct task_item it) { lock(&q->mu); while (q->count == BOUND) // while, never if wait(&q->not_full, &q->mu); q->items[(q->head + q->count) % BOUND] = it; q->count++; unlock(&q->mu); signal(&q->not_empty); // one task -> wake one worker } struct task_item q_take(struct bounded_queue *q) { lock(&q->mu); while (q->count == 0) // spurious wakeups re-checked wait(&q->not_empty, &q->mu); struct task_item it = q->items[q->head]; q->head = (q->head + 1) % BOUND; q->count--; unlock(&q->mu); signal(&q->not_full); // one slot -> wake one submitter return it; }

Narration points as you write it: the indices are plain ints because they're only ever touched under mu — one sentence that shows you know why the ring buffer chapter needed atomics and this doesn't. The while loops are lesson 02's rule 1 verbatim. And each signal targets the one condvar whose waiters can now make progress — push wakes an eater, take wakes a feeder.

The result handle

c
struct handle { mutex mu; condvar done_cv; enum { PENDING, DONE } state; union { result val; error err; } payload; bool is_error; }; void handle_complete(struct handle *h, result val, error err, bool is_err) { lock(&h->mu); h->payload = ...; h->is_error = is_err; h->state = DONE; // completes exactly once unlock(&h->mu); broadcast(&h->done_cv); // all waiters may proceed } result handle_wait(struct handle *h) { lock(&h->mu); while (h->state != DONE) // same discipline, miniature wait(&h->done_cv, &h->mu); unlock(&h->mu); if (h->is_error) rethrow(h->payload.err); // failure comes home HERE, return h->payload.val; // in the caller's thread }

Note the broadcast rather than signal: a handle may have several waiters (two threads both interested in one result), and DONE satisfies all of them — this is the case where waking everyone is correct, and saying why it differs from the queue's signal-one earns more than either line alone.

The worker loop

c
void worker_loop(struct pool *p) { forever { struct task_item it = q_take(&p->queue); // blocks when idle result val; error err; bool failed = false; TRY { val = it.task(it.a); } CATCH_ALL { err = current_error(); failed = true; } // NOTHING escapes handle_complete(it.h, val, err, failed); } }

The CATCH_ALL is invariant 4 made syntax. Say the sentence as you write it: "a task's exception is its submitter's business, so it travels through the handle and rethrows in the waiter's thread — the worker is just the courier." A worker that can die from user code turns a caller's bug into everyone's outage; this block is what forbids that.

submit and construction

c
struct handle *submit(struct pool *p, fn task, args a) { struct handle *h = handle_new(); // PENDING q_push(&p->queue, (struct task_item){ task, a, h }); return h; } struct pool *pool_new(int n_workers) { struct pool *p = alloc(); q_init(&p->queue); for (int i = 0; i < n_workers; i++) spawn_thread(worker_loop, p); // N identical workers return p; }

submit is deliberately thin — allocate a handle, push, return. All policy lives in q_push's full-queue behavior (this version blocks; lesson 04 walks the alternatives), and all mechanism lives in the queue and handle you've already defended.

What you'd say about complexity

submit and take are O(1) plus lock contention — and contention is the honest headline: every submitter and every worker crosses one mutex, so the pool's ceiling is how fast that lock can hand off. Fine for tasks that run milliseconds; the moment tasks shrink toward microseconds, the single lock is the bottleneck, which is exactly the door lesson 04 opens with work stealing.

Key takeaway

The implementation is four small pieces in a strict order of importance: a bounded queue whose while-loop waits and under-lock mutations transcribe the condvar discipline exactly, a one-shot handle that rethrows failures in the waiter's own thread, a worker loop whose catch-everything block is structural rather than stylistic, and a thin submit. Plain ints under a mutex, atomics nowhere — in a blocking design the lock carries the ordering, and saying so is part of the answer.

Enjoying the preview?

Create a free account to unlock the rest of this course, the in-browser judge, and live AI mock interviews.

Sign up free to continue