Structured Concurrency, Select, and Worker Pool
scope { ... } implements structured concurrency: all spawns inside the
block are awaited before continuing. If any child panics, its siblings are
cancelled. This eliminates orphan tasks and guarantees that resources released
after the scope are truly clean.
Two producers and one consumer in the same scope; the program only advances after all of them finish.
// Feature: structured concurrency — `scope { spawn ... }` joins on exit
// Syntax: `scope { ... }` spawns child tasks and waits for all of
// them before continuing. A panic in any child cancels its siblings.
// When to use: any group of related coroutines that should live and
// die together. Avoids dangling tasks and orphaned futures.
let ch = channel(0)
scope {
spawn {
ch.send("first")
}
spawn {
ch.send("second")
}
spawn {
let a = ch.recv()
let b = ch.recv()
// Sort for deterministic output regardless of scheduler order.
let xs = [a.__val, b.__val]
if xs[0] < xs[1] {
print(xs[0])
print(xs[1])
} else {
print(xs[1])
print(xs[0])
}
}
}
print("scope-exited")
// expected:
// first
// second
// scope-exited
select waits on multiple channels at the same time and fires the first
ready arm. Add after <duration> => { ... } for a timeout and
default => { ... } for non-blocking polling.
Selection between two channels, timeout with after 100ms, and immediate poll with default.
// Feature: `select` — wait on multiple channels at once
// Syntax:
// select {
// x := <- chA => ...
// y := <- chB => ...
// after 50ms => ...
// default => ...
// }
// Fires the first arm that is ready. `select` blocks, so it must
// run inside a coroutine. `after Ns` adds a timeout. `default`
// fires immediately if nothing is ready.
// When to use: multi-source consumers, timeouts, non-blocking polls.
let a = channel(1)
let b = channel(1)
scope {
spawn {
sleep 1ms
a.send("from-a")
}
spawn {
// Only `a` will be ready, so this arm fires.
select {
x := <- a => { print("a: {x}") }
y := <- b => { print("b: {y}") }
}
}
}
// expected: a: from-a
// Timeout via `after <duration>`.
let c = channel(1)
scope {
spawn {
select {
x := <- c => { print("got: {x}") }
after 100ms => { print("timeout") }
}
}
}
// expected: timeout
// Non-blocking poll via `default`.
let d = channel(1)
scope {
spawn {
select {
x := <- d => { print("got: {x}") }
default => { print("nothing-ready") }
}
}
}
// expected: nothing-ready
The worker pool pattern combines everything: a producer distributes work
over a jobs channel, N workers compete for items and send results to the
results channel, and an aggregator consumes the results. channel(0)
(rendezvous) applies backpressure across the entire pipeline automatically.
1 producer → 4 workers → 1 aggregator, with scope guaranteeing the join of all tasks.
// Feature: worker pool — fan-out + fan-in over channels
// Pattern: 1 producer feeds a `jobs` channel, N workers compete for jobs
// and push results into a `results` channel, 1 aggregator drains them.
// `scope { }` joins everything: when the last spawn finishes, the block
// exits. With `channel(0)` (rendezvous) you get backpressure for free —
// workers block on send until the aggregator is ready, and the producer
// blocks on send until a worker is ready.
let jobs = channel(0)
let results = channel(0)
let total = 8
scope {
spawn {
for i in 0..total { jobs.send(i) }
jobs.close()
}
// 4 workers — each loops on `for n in jobs` until the channel closes.
spawn { for n in jobs { results.send(n * n) } }
spawn { for n in jobs { results.send(n * n) } }
spawn { for n in jobs { results.send(n * n) } }
spawn { for n in jobs { results.send(n * n) } }
// Aggregator: knows how many results to expect, closes the channel
// when the last one lands so the scope can complete.
spawn {
var got = 0
for r in results {
print("result: {r}")
got += 1
if got == total { results.close() }
}
}
}
print("done")
// expected (in some interleaved order — squares of 0..7 then "done"):
// result: 0, 1, 4, 9, 16, 25, 36, 49
// done
Deadlocks are noisy, not silent. A
scopewhose tasks are all blocked on channels with no possible unlocker throwsdeadlock: ...instead of hanging forever.