Concurrency
If a program waits on a network read, then a disk write, then another network read — it spends most of its time blocked on something else's clock. The fix is to do other useful work during those waits. There are several ways to do this; Glide picks one specific shape inspired by Go.
The two ways Glide schedules work
You'll meet two keywords. They look nearly identical but do very different things underneath.
- `spawn` — start a coroutine (a.k.a. "go-routine" in Go). Cheap: a coroutine costs ~192 bytes of memory in Glide. The runtime can schedule millions of them across just a few OS threads (this is what "M:N" means — M coroutines on N threads). Use this for most things.
- `spawn_thread` — start a real OS thread. Expensive (~1MB stack each), but each one is truly independent. Use this when you need to call something that blocks an OS thread (a synchronous C library, a blocking syscall the runtime can't hook).
In day-to-day Glide code, you'll use `spawn` almost exclusively.
fn worker(id: i32) {
println!("worker", id, "starting");
// ... do something
println!("worker", id, "done");
}
fn main() -> i32 {
spawn worker(1);
spawn worker(2);
spawn worker(3);
// wait for them somehow — see channels below
return 0;
}
Channels: how coroutines talk
Coroutines don't share variables by default — they communicate through channels. A channel is a typed pipe: one end you .send(value) into, the other end you .recv() from.
fn producer(c: chan<i32>) {
for i in 0..5 {
c.send(i * i);
}
c.close();
}
fn main() -> i32 {
let c: chan<i32> = make_chan(2); // buffered: holds up to 2 pending values
spawn producer(c);
while let v = c.recv() { // drains the channel until it's closed
println!("got:", v);
}
return 0;
}
What's happening:
make_chan(2)creates a buffered channel that can hold 2 values beforesendblocks.c.send(i * i)blocks if the buffer is full.c.recv()blocks if the buffer is empty, then returns the value directly — aT, not a wrapper. On a closed, drained channel it returns a zero-valuedT.while let v = c.recv() { ... }is the idiom for "drain until close": it pulls values and stops cleanly once the channel is closed and empty. (There's noloopkeyword in Glide — usewhile lethere, orwhile true { ... }for an unconditional loop.)c.close()signals "no more values coming."
Unbuffered channels
Pass 0 to make_chan for a synchronous channel — sender and receiver have to meet at the same point in time. Useful for handshakes:
let ready: chan<i32> = make_chan(0);
spawn function_that_signals_when_ready(ready);
ready.recv(); // blocks until the spawnee sends something
println!("the worker said it's ready");
select!: waiting on multiple channels
When a coroutine wants to wait for whichever of several events fires first, use select!. Same shape as Go's select or Erlang's receive.
import stdlib::time::*;
fn main() -> i32 {
let a: chan<string> = make_chan(0);
let b: chan<string> = make_chan(0);
let timeout: chan<i64> = after(Duration::from_millis(2000)); // fires after 2s
spawn slow_query(a);
spawn slow_query(b);
select! {
msg = a.recv() => { println!("a said:", msg); }
msg = b.recv() => { println!("b said:", msg); }
_ = timeout.recv() => { println!("timed out"); }
}
return 0;
}
Each arm is binding = chan.recv() => { ... }. The recv() hands you the value directly, so you use msg (not msg.val) inside the arm; use _ when you only care that the channel fired. The first arm whose channel is ready wins; the rest are abandoned. If several are ready at once, select! picks one (currently in arm order; future versions may randomize).
A worker pool
Putting it together: spawn N worker coroutines, feed them jobs through a channel, collect results from another channel. Classic pattern, ~25 lines:
fn worker(id: i32, jobs: chan<i32>, results: chan<i32>) {
while let j = jobs.recv() { // drains until jobs is closed
results.send(j * 2); // pretend the work is `value * 2`
}
println!("worker", id, "exiting");
}
fn main() -> i32 {
let jobs: chan<i32> = make_chan(10);
let results: chan<i32> = make_chan(10);
for w in 1..=3 { spawn worker(w, jobs, results); }
// queue 8 jobs
for j in 1..=8 { jobs.send(j); }
jobs.close();
// collect 8 results
for _ in 0..8 {
let r: i32 = results.recv();
println!("result:", r);
}
return 0;
}
Three workers share the queue. The first to be free picks up the next job. Channel-based, no shared memory, no locks.
When you do need shared state
Sometimes one piece of data really has to be touched by many coroutines — a counter, a cache, a registry. Use stdlib::sync::Mutex<T>:
import stdlib::sync::*;
fn increment(m: *Mutex<i32>) {
m.lock();
m.set(m.get() + 1); // read + write the guarded value
m.unlock();
}
fn main() -> i32 {
let m: *Mutex<i32> = Mutex::new(0);
increment(m);
increment(m);
println!(m.get()); // 2
return 0;
}
m.lock() blocks until the mutex is free; m.get() / m.set(v) read and write the guarded value; m.unlock() releases it. Lock and unlock come in pairs — every lock() needs a matching unlock() on every path out. If you'd rather not track that by hand, m.with(fn) runs a function while holding the lock and unlocks for you.
When to use spawn_thread
If you're calling something that blocks the OS thread — a synchronous C library that doesn't cooperate with the runtime — you need a real OS thread so it doesn't freeze the coroutine scheduler:
spawn_thread call_into_blocking_c_library();
In normal Glide code (stdlib I/O, HTTP, channels, sleep), the runtime hooks blocking calls and parks the coroutine instead of the thread. So you rarely need spawn_thread.
A real example: a tiny HTTP echo server
Putting spawn + channels + I/O together. This is a fully-working server:
import stdlib::net::listener::*;
fn handle(c: *Conn) {
let buf: *u8 = malloc(4096) as *u8;
defer free(buf as *void);
let n: i32 = c.read(buf as *void, 4096);
if n > 0 { c.write(buf as *void, n); }
c.close();
}
fn main() -> i32 {
let l: *Listener = Listener::bind(8080);
if !l.ok() { return 1; }
println!("echo server on :8080");
while true {
let c: *Conn = l.accept();
spawn handle(c); // each connection gets its own coroutine
}
return 0;
}
Each incoming connection becomes its own coroutine — the runtime can handle hundreds of thousands of them concurrently on a couple of OS threads.
Where to next
You can spawn coroutines, wire them with channels, and time out cleanly with select!. The last chapter — Modules and packages — covers how to organize multiple files into a project and pull in external code.