// lesson: bounded-parallelism
Bounded Parallelism
go f() is so cheap that the instinct is to launch one goroutine per
item. For CPU work that just adds scheduler pressure past GOMAXPROCS;
for I/O work it is worse โ ten thousand goroutines means ten thousand
simultaneous connections hammering a database that wanted fifty. The
goroutines are cheap; what they do is not. Concurrency limits protect
the thing downstream.
The lightest-weight limiter is a buffered channel used as a counting semaphore:
sem := make(chan struct{}, limit)
for _, item := range items {
sem <- struct{}{} // blocks while `limit` are in flight
go func(item Item) {
defer func() { <-sem }()
process(item)
}(item)
}
Acquire by sending, release by receiving; the buffer size is the
limit. The alternative shape โ a fixed pool of limit worker goroutines
ranging over a shared channel โ does the same job and is preferable
when workers carry per-worker state (a connection, a buffer).
Two details separate a toy from a correct implementation. Order:
results must often line up with inputs even though completion order is
arbitrary. Do not collect from a shared channel and re-sort โ have
goroutine i write to out[i]. Distinct indices of a slice are
distinct memory; there is no race, and the WaitGroup.Wait edge makes
every write visible to the reader afterwards. Failure: when one
item fails, the remaining work is usually garbage. Combine the
semaphore with a derived context: record the first error, cancel, and
stop dispatching. That is the shape of the next challenge โ and, not
coincidentally, of errgroup, which you will build right after.
โบ Order-Preserving ParallelMap
35 ptsImplement:
func ParallelMap(ctx context.Context, in []int, limit int, f func(context.Context, int) (int, error)) ([]int, error)
Semantics (assume limit >= 1):
- Apply
fto every element, running at mostlimitcalls tofconcurrently โ and actually use the budget: calls must overlap. out[i]corresponds toin[i]โ results keep input order.- Pass each
fa context derived fromctx. When any call returns an error, cancel that context promptly, stop dispatching new work, and return the first error (the returned slice is then unspecified). - Empty input returns an empty slice and nil error.
- Return only after every in-flight call has finished.
Log in to submit a solution and earn points.