Concurrency
Lattice provides structured concurrency through scope blocks,
spawn expressions, channels, and select multiplexing. The phase
system integrates with concurrency to prevent data races: only crystal (frozen) values
can be sent across channels.
Scope Blocks
scope_expr ::= "scope" block
A scope block creates a structured concurrency context. All spawn
tasks launched within a scope block must complete before the scope exits. This guarantees
that no spawned task outlives its parent scope.
scope { spawn { print("task 1") } spawn { print("task 2") } spawn { print("task 3") } } // All three tasks are complete here
Spawn
spawn_expr ::= "spawn" block
The spawn expression launches a new task (thread) that executes the given block
concurrently. Spawn must appear inside a scope block. Each spawned task gets
its own thread and garbage collector.
Spawned tasks receive deep copies of captured variables. Modifications to local variables inside a spawned task do not affect the parent scope. Use channels for communication between tasks.
Channels
Channels are typed communication primitives for passing values between concurrent tasks.
A channel is created with Channel::new() and supports three operations:
| Method | Description |
|---|---|
ch.send(value) | Send a value into the channel. The value must be crystal. |
ch.recv() | Receive a value from the channel. Blocks until a value is available. |
ch.close() | Close the channel. Further sends raise an error. |
freeze() before sending.
let ch = Channel::new() scope { spawn { let result = expensive_computation() ch.send(freeze(result)) } } let value = ch.recv() print("got: ${value}")
Select Expressions
select_expr ::= "select" "{" { select_arm } "}" select_arm ::= identifier "from" expression "=>" block | "default" "=>" block | "timeout" "(" expression ")" "=>" block
select multiplexes across multiple channels, waiting for the first one that
has a value available. Each arm binds the received value to a variable.
Two special arms are available:
default— Executes immediately if no channel has data ready.timeout(ms)— Executes if no channel delivers within the specified milliseconds.
let ch1 = Channel::new() let ch2 = Channel::new() scope { spawn { ch1.send(freeze("hello")) } spawn { ch2.send(freeze(42)) } } select { msg from ch1 => { print("ch1: ${msg}") }, val from ch2 => { print("ch2: ${val}") }, timeout(1000) => { print("timed out") } }
Data Race Prevention
The phase system and structured concurrency work together to prevent data races:
- Spawned tasks receive deep copies of captured variables, not references.
- Channels only accept crystal values, ensuring immutability across threads.
- Scope blocks guarantee all tasks complete before shared state is accessed.
This design makes it impossible for two concurrent tasks to mutate the same data simultaneously. Communication through channels replaces shared mutable state.
Common Patterns
Fan-Out / Fan-In
fn parallel_map(items: Array, f: Fn) -> Array { flux results = [] flux channels = [] for item in items { let ch = Channel::new() channels.push(ch) } scope { for i in 0..items.len() { let ch = channels[i] let item = items[i] spawn { ch.send(freeze(f(item))) } } } for ch in channels { results.push(ch.recv()) } return results }
Lattice