Concurrency
XS has real OS threads via spawn, async/await for I/O, channels for message passing, actors for encapsulated state, and nurseries for structured concurrency.
spawn
spawn runs a block on a real OS thread. Bytecode execution is GIL-serialised, so two pure-compute threads take turns rather than running fully in parallel. Blocking calls (time.sleep, channel ops, I/O) release the GIL, so one thread can run while another waits.
var done = false
let t = spawn { done = true }
println(done) -- false (spawn runs concurrently)
await t
println(done) -- true
-- 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 queues. Unbounded by default; pass a capacity to make them bounded. 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
-- bounded channel
let bch = channel(2)
bch.send("a")
bch.send("b")
println(bch.is_full()) -- true
println(bch.recv()) -- aclose() marks a channel done. Subsequent send raises ChannelClosed; recv on a drained closed channel returns null. Use recv_pair() when the channel can carry real nulls:
let ch = channel(2)
ch.send(null)
ch.send("hi")
ch.close()
ch.recv_pair() -- (null, true) -- real null was sent
ch.recv_pair() -- ("hi", true)
ch.recv_pair() -- (null, false) -- closed and drainedselect([ch1, ch2, ...]) returns {index, value} for the first channel with a buffered value. Returns null when nothing is ready.
Actors
Actors encapsulate mutable state and handle method calls or raw messages. They are spawned once and run as a persistent thread.
actor BankAccount {
var balance = 0
fn deposit(amount) {
balance += amount
}
fn withdraw(amount) {
if amount > balance { return Err("insufficient funds") }
balance -= amount
return Ok(balance)
}
fn get_balance() { return balance }
fn handle(msg) {
if msg == "reset" { balance = 0 }
}
}
let acct = spawn BankAccount
acct.deposit(100)
acct.deposit(50)
println(acct.get_balance()) -- 150
acct ! "reset" -- send raw message
println(acct.get_balance()) -- 0Nurseries
A nursery block waits for all tasks spawned inside it to finish before continuing. No tasks leak out. When one task throws, the surviving siblings receive a cancellation signal and unwind cleanly.
var results = []
nursery {
spawn { results.push("a") }
spawn { results.push("b") }
spawn { results.push("c") }
}
-- all tasks complete before we reach here
println(results.sort()) -- ["a", "b", "c"]-- producer/consumer with a nursery
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]