fork-join and the pipeline pattern
Two everyday shapes of parallel work. The first: to count books in a huge library, split the building into wings, send a team to each wing simultaneously, then add up their counts. That is fork-join — fork the work into independent sub-tasks that run in parallel, then join (wait for all and combine the results). The second: a car assembly line, where each station does one step and passes the car to the next, so many cars are in progress at once at different stations. That is a pipeline — work flows through a sequence of stages, each stage running concurrently on a different item.
Fork-join is the divide-and-conquer pattern: a task splits its problem into smaller independent pieces (fork), those pieces run concurrently (ideally on a work-stealing pool, which suits the recursive shape perfectly), and the parent blocks at the join until all children finish, then merges their results. parallel-for is the flat special case — apply the same operation to every element of an array across many workers — and map-reduce is fork-join over data: map each chunk in parallel, then reduce (combine) the partial results. A pipeline (or dataflow) instead chains stages s1 -> s2 -> s3, connected by channels: stage 1 produces items into a channel, stage 2 consumes and transforms them into the next channel, and so on. Because the stages run on different threads, while stage 2 works on item 5, stage 1 is already producing item 6 — throughput is limited by the slowest stage, not the sum of all stages. The two patterns compose: a pipeline stage can itself fork-join internally.
These matter because most real parallelism reduces to one of these shapes, and naming them tells you how to reason about each. Fork-join is about latency on a single big task: more cores finish it sooner, and Amdahl's law bounds the speedup by the part you could not parallelise. Pipelines are about throughput on a stream: they overlap stages but a single item still travels through every stage in series, so they cut throughput limits, not per-item latency. Honest caveats: fork-join only helps if the sub-tasks are genuinely independent and big enough to outweigh the cost of spawning and joining them — splitting too finely makes overhead dominate; and a pipeline is only as fast as its slowest stage, and needs backpressure on its channels so a fast stage cannot flood a slow one.
Fork-join sum (Rust/rayon): let total = data.par_iter().map(square).sum(); // forks across the pool, joins the partials — a map-reduce. Pipeline: producer -> channel -> parse -> channel -> write, three stages on three threads, overlapping.
Fork-join cuts the latency of one big task; a pipeline overlaps stages to raise throughput of a stream.
Splitting too finely makes spawn/join overhead dominate — fork-join needs sub-tasks big enough to be worth it. A pipeline is only as fast as its slowest stage, and without backpressure a fast stage floods a slow one. Naming the pattern is the first step to sizing it right.