Concurrency
Spawn, async/await, channels, actors, and nurseries for structured concurrency.
Summary
spawn { block } runs on a real OS thread; await tblocks until it finishes and returns the body's value. Bytecode execution is GIL-serialized, but blocking calls (sleep, channel recv, I/O) release the GIL so siblings actually run. Channels are FIFO queues; bounded channels block the sender when full. close() marks a channel done; recv on a drained closed channel returns null; recv_pair() distinguishes a sent null from a closed channel. select([ch1, ch2]) returns the first ready channel. Actors encapsulate state and respond to typed method calls or raw messages via !. Nurseries guarantee all spawned tasks finish before the nursery exits; one task throwing cancels siblings cooperatively.
Canonical
Spawn
spawn runs a block on a real OS thread. The body returns a future the parent can await for the value or hand to a nursery for structured-concurrency cleanup. Bytecode execution is GIL-serialized so two pure-compute spawns trade off rather than running on different cores at once, but blocking calls (time.sleep, channel send / recv, io reads) release the GIL so a sibling actually runs while one is waiting.
var done = false
let t = spawn { done = true }
println(done) -- false (spawn runs concurrently)
await t
println(done) -- true (worker has finished)
-- await returns the body's value
let result = spawn { 1 + 2 }
println(await result) -- 3Async / Await
async fn compute(x) {
return x * 2
}
let r = await compute(21)
println(r) -- 42
async fn fetch_user(id) {
return #{"id": id, "name": "User {id}"}
}
let user = await fetch_user(42)
println(user["name"]) -- User 42Channels
Channels are FIFO message queues. Unbounded by default, or bounded with a capacity. A bounded channel's send blocks while the buffer is full.
-- unbounded channel
let ch = channel()
ch.send("ping")
ch.send("pong")
println(ch.recv()) -- ping
println(ch.recv()) -- pong
println(ch.len()) -- 0
println(ch.is_empty()) -- true
-- bounded channel: send blocks once cap is reached
let bch = channel(2)
bch.send("a")
bch.send("b")
println(bch.is_full()) -- true
println(bch.recv()) -- a
println(bch.is_full()) -- falseclose() marks a channel done. Subsequent send raises ChannelClosed; recv on a drained closed channel returns null instead of blocking forever. When the channel itself can carry null values, use recv_pair() for a Go-style (value, ok) tuple where ok=false distinguishes "closed" from "received null":
let ch = channel(2)
ch.send(null)
ch.send("hi")
ch.close()
ch.recv_pair() -- (null, true) -- a real null was sent
ch.recv_pair() -- (hi, true)
ch.recv_pair() -- (null, false) -- closed and drainedIterating with for v in ch drains the currently-buffered values:
let q = channel()
q.send(1); q.send(2); q.send(3)
q.close()
var got = []
for v in q { got.push(v) }
println(got) -- [1, 2, 3]
println(q.is_closed()) -- trueselect([ch1, ch2, ...]) returns {index, value} for the first channel with a buffered value (also accepts task futures, returning their _result). Returns null when nothing is ready -- for blocking semantics, loop until you get a hit:
let a = channel()
let b = channel()
a.send("hi")
let r = select([a, b])
println(r.index, r.value) -- 0 hiActors
Actors encapsulate state and respond to method calls or raw messages.
actor BankAccount {
var balance = 0
fn deposit(amount) {
balance = balance + amount
}
fn withdraw(amount) {
if amount > balance { return Err("insufficient funds") }
balance = balance - amount
return Ok(balance)
}
fn get_balance() { return balance }
-- handle() processes raw messages sent with !
fn handle(msg) {
if msg == "reset" { balance = 0 }
}
}
let acct = spawn BankAccount
acct.deposit(100)
acct.deposit(50)
println(acct.get_balance()) -- 150
-- send raw message with !
acct ! "reset"
println(acct.get_balance()) -- 0Nursery (Structured Concurrency)
Nursery blocks wait for all spawned tasks to finish before continuing. No tasks leak out.
var results = []
nursery {
spawn { results.push("a") }
spawn { results.push("b") }
spawn { results.push("c") }
}
-- all tasks complete before we get here
println(results.sort()) -- ["a", "b", "c"]Nurseries compose with channels for producer/consumer patterns:
let pipe = channel()
var output = []
nursery {
spawn {
for i in 1..=3 { pipe.send(i * 10) }
}
spawn {
for i in 0..3 { output.push(pipe.recv()) }
}
}
println(output) -- [10, 20, 30]When one task in a nursery throws, the surviving siblings get a cooperative cancellation flag. Blocking primitives (time.sleep, channel recv) check it on resume and raise Cancelled, so a sleeping sibling unwinds cleanly instead of finishing its body. The nursery surfaces the original throw, suppressing the cleanup-noise Cancelled errors.