--- a/pipeline.c 2026-09-23 13:35:04 +++ b/pipeline.c 2026-09-23 13:35:04 @@ -1,20 +1,20 @@ -/* EXERCISE 03 — BROKEN ON PURPOSE. Do not copy this shape into real code. +/* EXERCISE 03 — REPAIRED. * * An ingest pipeline. Frames arrive from a capture device roughly every 5 ms. * A pool of worker threads picks each frame up and hashes it. * - * Symptom as reported by the field: "the fans spin up and the battery drains - * even when almost nothing is arriving. Activity Monitor shows us pinned near - * 100% of several cores while the frame rate is only 200 per second." + * The repair: an idle worker BLOCKS instead of spinning. A blocked thread is + * off every run queue, so it costs a stack and a scheduler slot and no CPU at + * all; the kernel makes it runnable again when the producer signals. The + * condition variable is paired with the mutex that guards the state it tests, + * and the wait sits in a loop because a wakeup is a hint, not a promise. * * Build and run: - * clang -O2 -g -Wall -Wextra -pthread pipeline.c -o /tmp/pipeline_broken - * /tmp/pipeline_broken + * clang -O2 -g -Wall -Wextra -pthread pipeline.c -o /tmp/pipeline_fixed + * /tmp/pipeline_fixed * - * The output is CORRECT. Every frame is processed exactly once, and the - * checksum is right. This exercise is not about correctness. - * - * Every measurement is printed as key=value so a script can assert on it. + * The frames processed and the checksum are unchanged. Only the CPU cost of + * waiting is different. Every measurement is printed as key=value. */ #include #include @@ -44,12 +44,18 @@ } /* ------------------------------------------------------------ the pipeline */ -/* One slot. The producer publishes a sequence number; consumers claim it. */ -static atomic_int published = 0; /* last frame number made available */ -static atomic_int claimed = 0; /* last frame number taken by a worker */ +/* One slot. The producer publishes a sequence number; consumers claim it. + * + * `published`, `claimed` and `shutting_down` are now guarded by `m` rather + * than being free-standing atomics, because the condition variable and the + * predicate it tests must be protected by the same mutex. */ +static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t work = PTHREAD_COND_INITIALIZER; +static int published = 0; /* last frame number made available */ +static int claimed = 0; /* last frame number taken by a worker */ static atomic_int processed = 0; static atomic_ullong checksum = 0; -static atomic_int shutting_down = 0; +static int shutting_down = 0; static void hash_frame(int seq) { unsigned long long h = 1469598103934665603ULL; @@ -61,23 +67,19 @@ static void *worker(void *unused) { (void)unused; for (;;) { + int mine; + pthread_mutex_lock(&m); /* Wait for a frame to become available. * - * This loop never leaves the CPU. The thread stays RUNNABLE, so the - * scheduler keeps handing it a core to do nothing on. */ - int mine; - for (;;) { - if (atomic_load_explicit(&shutting_down, memory_order_acquire)) return NULL; - int avail = atomic_load_explicit(&published, memory_order_acquire); - int taken = atomic_load_explicit(&claimed, memory_order_relaxed); - if (taken < avail) { - mine = taken + 1; - if (atomic_compare_exchange_weak_explicit( - &claimed, &taken, mine, - memory_order_acq_rel, memory_order_relaxed)) break; - } - /* spin again */ - } + * pthread_cond_wait releases the mutex and parks this thread in the + * kernel. It is in a WHILE loop, not an if: a wakeup does not prove + * the predicate, and several workers can be woken for one frame. */ + while (claimed >= published && !shutting_down) + pthread_cond_wait(&work, &m); + if (shutting_down && claimed >= published) { pthread_mutex_unlock(&m); return NULL; } + mine = ++claimed; + pthread_mutex_unlock(&m); + hash_frame(mine); } } @@ -99,12 +101,18 @@ for (int f = 1; f <= FRAMES; f++) { usleep(FRAME_GAP_US); - atomic_store_explicit(&published, f, memory_order_release); + pthread_mutex_lock(&m); + published = f; + pthread_cond_signal(&work); /* one frame, one waiter to wake */ + pthread_mutex_unlock(&m); } /* Wait for the last frames to drain. */ while (atomic_load(&processed) < FRAMES) usleep(1000); - atomic_store_explicit(&shutting_down, 1, memory_order_release); + pthread_mutex_lock(&m); + shutting_down = 1; + pthread_cond_broadcast(&work); /* shutdown concerns every waiter */ + pthread_mutex_unlock(&m); for (int i = 0; i < nworkers; i++) pthread_join(t[i], NULL); double wall = now_ms() - t0;