/ Specification
Playground Docs Performance GitHub
Chapter 10

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
Note Scope blocks wait for all spawned tasks to complete before continuing. This is the fundamental guarantee of structured concurrency.

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:

MethodDescription
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.
Important Only crystal (frozen) values can be sent through channels. Attempting to send a fluid value raises a runtime error. Use 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:

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:

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
}