// 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 pts

Implement:

func ParallelMap(ctx context.Context, in []int, limit int, f func(context.Context, int) (int, error)) ([]int, error)

Semantics (assume limit >= 1):

  • Apply f to every element, running at most limit calls to f concurrently โ€” and actually use the budget: calls must overlap.
  • out[i] corresponds to in[i] โ€” results keep input order.
  • Pass each f a context derived from ctx. 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.