Day 071 — Worker Pool Pattern¶
Month 3 · Week 3 · ⬅ Day 070 · Day 072 ➡ · Journal index
🎯 Learning Objective¶
Build a reusable worker-pool: a fixed set of N goroutines that pull jobs from a channel and push results to another, so concurrency stays bounded no matter how much work arrives.
📚 Topics¶
- Goroutine-per-task (unbounded) vs worker pool (bounded
N) - Jobs channel · results channel · the closer goroutine
- Backpressure from an unbuffered/bounded jobs channel
📖 Reading / Sources¶
- Go blog — Go Concurrency Patterns: Pipelines and cancellation
- Effective Go — Channels
-
sync.WaitGroupdocs
📝 Notes¶
- A worker pool decouples how much work exists from how much runs at once. Spawn exactly
Nlong-lived goroutines, each ranging over a sharedjobschannel → [[worker-pool]]. - One goroutine per task is fine for a handful, but unbounded for 1M tasks: it can exhaust memory, file descriptors, or hammer a downstream service. The pool caps that at
N→ [[bounded-concurrency]]. - All workers
rangethe samejobschannel — Go delivers each value to exactly one receiver, so jobs are load-balanced automatically (whoever is free reads next) → [[channel-axioms]]. close(jobs)is the stop signal: every worker'srangeloop ends when the channel is drained and closed. Close from the single producer, after the last job is queued.- Collect results on a separate
resultschannel. Closeresultsin a closer goroutine (wg.Wait(); close(results)) so the consumer'srangeterminates exactly once → [[close-once-closer]]. - An unbuffered
jobschannel gives backpressure: the producer blocks until a worker is ready, so the queue can't balloon. Buffer it only to smooth bursts → [[backpressure]]. - Worker-pool results arrive out of order (whichever worker finishes first). Tag each job with an index if you need to restore input order.
💻 Code Examples¶
func pool[T, R any](workers int, in []T, fn func(T) R) []R {
jobs := make(chan int) // send indices, not values, to keep order
results := make([]R, len(in))
var wg sync.WaitGroup
wg.Add(workers)
for w := 0; w < workers; w++ {
go func() {
defer wg.Done()
for i := range jobs { // each worker pulls the next free index
results[i] = fn(in[i])
}
}()
}
for i := range in {
jobs <- i
}
close(jobs) // workers' range loops end once drained
wg.Wait()
return results
}
Full code:
examples/month-03/workerpool/main.go· Run:go run ./examples/month-03/workerpool
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
| Counting semaphore (bounded parallelism core) | ✅ | exercises/month-03/week-3/semaphore/ |
Order-preserving bounded Map with context |
✅ | exercises/month-03/week-3/boundedmap/ |
🐛 Mistakes Made¶
- Closed
jobsfrom inside a worker → the other workers panicked on the next send. Close belongs to the single producer. - Forgot the closer goroutine and
closedresultsright after queuing jobs → workers were still writing → send-on-closed panic. The closer must wait onwg.
❓ Open Questions¶
- How do I size the pool? (Rule of thumb: CPU-bound → ~
GOMAXPROCS; I/O-bound → higher, tune by benchmark. More on this Day 074.)
🧠 Active Recall (answer without looking)¶
-
Q: Why does sending each value to a shared
jobschannel load-balance automatically?
A
A channel delivers every sent value to exactly one receiver; whichever worker is currently blocked in `range` (i.e. free) takes the next job. No dispatcher logic needed. -
Q: Who should
close(jobs)and who shouldclose(results)?
A
The single producer closes `jobs` after the last job. A dedicated closer goroutine closes `results` after `wg.Wait()` confirms every worker has finished writing.
🪶 Feynman Reflection¶
A worker pool is a small crew at a conveyor belt. Tasks ride in on one belt (jobs); each free worker grabs the next one; finished work rides out on a second belt (results). The crew size is fixed, so a flood of tasks just queues up — it never spawns a million workers and crashes the warehouse.
🕳️ Knowledge Gaps¶
- Returning errors from workers and cancelling the rest — that's
errgroupterritory (Day 076).
✅ Summary¶
I can build a bounded worker pool: N goroutines ranging a jobs channel, results gathered via index or a results channel closed by a closer goroutine after wg.Wait().
⏭️ Next Steps / Prep for Tomorrow¶
- Day 072: generalise this into the fan-out / fan-in vocabulary.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: feat(examples): worker pool pattern (day 071)