Three earlier pages each showed one face of the same thing.
Channel streams put a producer behind a bounded
ring and later joined a second one with into. A room is a
stream leaned on that fan-in for its relay.
The worker pool went the other way — one stream, N
pulling workers. This page says out loud what those pages implied: underneath
all of them sits one machine, the bounded multi-producer multi-consumer
ring, and the surface’s spellings are not features stacked on features — each
one states a fact about that ring. Then it runs both directions at once.
The example is examples/MpmcPipeline
in the tutorial repository: three producers, one ring, four workers.
Getting the file
make -C examples/MpmcPipeline run
make -C examples/MpmcPipeline run MICA_EXTRA_FLAGS="--tasking multicore,4"Both commands — one carrier or four — print the same two lines:
three producers, one ring, four workers
delivered 24 of 24
sum 48108 - every value in exactly one workerEach producer’s values carry its tag in the thousands, so all 24 values are unique and that sum can only be 48108 if every value was delivered exactly once — the self-checking output, in the concurrent world, where it earns its keep for real.
The example, walked
The ring, said three ways
s := schedule Produce(1, PerProducer) buffer 4;
schedule Produce(2, PerProducer) into s;
schedule Produce(3, PerProducer) into s;
concurrent
schedule Worker(0, s);
schedule Worker(1, s);
schedule Worker(2, s);
schedule Worker(3, s);
end;Eleven lines, three spellings, each a static fact the compiler holds:
schedule … buffer 4bindssto a ring and starts its first producer. Ring-ness is a fact of the bind, not a runtime discovery.into sjoins another producer to the ring that is already open — fan-in is a second attach, never a second mechanism.sas astream oftask parameter is what admits four workers pulling one variable — the fan-out the ring was always able to serve, stated where the compiler can check it.
Every value is popped by exactly one worker. Completeness is the ring’s promise; placement belongs to the scheduler, which is why the program prints joined totals and no per-worker breakdown — the honest report is the one no interleaving can change.
Partition, don’t share
task Worker(w : int64, jobs : stream of int64);
...
for job in jobs do
begin
c := c + 1;
t := t + job;
end;
synchronized count[w] := c;
synchronized total[w] := t;The hot loop takes no lock and needs none: each worker tallies into its own
locals, and each parks its result in its own slot — count[w], nobody
else’s. synchronized appears exactly twice in the whole program, where a
finished worker makes its slot visible to the owner that joins the scope.
That is the design rule this page most wants you to take away: partition
state so no lock is needed on the hot path, and let synchronized mark the
one place sharing genuinely happens. The compiler enforces the marking — the
second experiment shows it refusing the unmarked touch.
Backpressure, and the end
The buffer 4 is the whole flow-control policy. A producer that runs ahead
parks on the full ring; a worker that outruns production parks on the empty
one; nothing spins, nothing drops, and no knob exists to mistune. And the
stream ends when the last of the three producers ends — an into-joined
ring is not done until everyone who feeds it is.
Merge, or select
into and select both face several producers; they answer different
questions. Merge with into when the values are one workload and arrival
order is yours to surrender — you get one stream, exactly-once delivery, and
totals like this page’s. Reach for select — taught at
channel streams — when the sources must stay
distinguishable: who spoke first matters, or a timeout arm needs to catch
nobody speaking at all. One ring merging, or several rings raced; the design
choice is which question your consumer is really asking.
Try it
1. Four carriers. Run the second command from the top of the page five times if you like — the same two lines, every time. The totals are not usually-right; they are the ring’s promise.
2. Unmark the sharing. Remove synchronized from the count[w] := c;
line and rebuild:
analyzer error 5337: a task may touch the program's global variable 'count' only in a 'synchronized'-marked statement: while tasks may be running, a global is shared state on every core, so the access must be spelled 'synchronized' to make that sharing visible — outside the shared region no mark is neededThe data-race law from the data-races page, holding in the middle of a real pipeline: sharing is spelled where it happens, or the program does not build.
3. Lose the ring. Change the bind line to
s := Produce(1, PerProducer); — a pull chain instead of a ring — and
rebuild:
analyzer error 5393: the 'into' target must be a stream variable already bound by 'schedule', but 's' is not: the clause joins a second producer to an existing ring — bind the stream first with '<variable> := schedule <generator>(...)'Fan-in needs the ring, and the refusal says so with the fix spelled out — ring-ness is a static fact, which is exactly why all of this can be checked before anything runs.
What this page proved
Exactly-once delivery is printed, not promised: 24 of 24, sum 48108, one carrier or four, on both architectures. The sharing discipline is compiled, not reviewed: the unmarked touch and the ring-less fan-in are refusals with their fixes in the message. And the machine underneath — the runtime’s ring — is exercised under ThreadSanitizer in the compiler’s own gates on every merge, which is what lets a tutorial print “every value in exactly one worker” as a fact rather than a hope.
Next
The streams shelf, in one breath: generators pull, channel streams push and select, the worker pool fans out, the chat server fans in across a network, and this page ran both directions through the one ring underneath them all. Every example lives in the tutorial repository.