moon offers two complementary ways to do work at the same time:
- async tasks overlap waiting for timers, files, network requests, and other I/O;
- isolates and
std.paralleluse multiple CPU cores for compute-heavy work.
Channels pass values between async tasks. Isolates do not share mutable state, which keeps parallel code predictable.
Async and await
An async fn returns a Future<T>. Use await inside another async function to wait for its result:
async fn fetch_number() -> int {
await sleep(100)
42
}
async fn main() {
println(await fetch_number())
}A future starts when it is awaited or passed to spawn. This makes it safe to build the work first and choose later whether to run it sequentially or concurrently.
Start several tasks
spawn starts one future without waiting for it immediately. join_all waits for a list of futures and returns results in input order:
async fn load(name: string, delay: int) -> string {
await sleep(delay)
name
}
async fn main() {
let jobs = [
spawn(load("profile", 20)),
spawn(load("settings", 10)),
]
println(await join_all(jobs))
}Use join((a(), b(), c())) when the futures return different types. Use select(futures) when you need the index of the first future to finish.
Cancellation and errors
cancel(future) requests cancellation. Cancellation is cooperative: the task stops at a point where it can safely yield, so application cleanup still runs.
Use Result<T, Error> for expected failures such as validation errors or missing records. Future.catch is for a task that fails unexpectedly while it is running:
async fn main() {
let value = await risky_work()
.catch(|failure| {
println(failure.message)
0
})
println(value)
}Channels
std.channel provides bounded first-in, first-out channels between async tasks. Sending waits when the channel is full; receiving waits when it is empty. Once a channel is closed and drained, recv returns none.
async fn produce(channel: Channel) {
for value in 1..4 {
await channel.send(value)
}
channel.close()
}
async fn main() {
let channel = std.channel.new(4)
spawn(produce(channel))
let mut open = true
while open {
match await channel.recv() {
some(value) => println(value),
none => open = false,
}
}
}A capacity of 0 creates a rendezvous channel: every send waits for a matching receive.
Isolates
Use an isolate for CPU-heavy work that should run on another core. An isolate runs a named top-level function and exchanges plain values with the caller.
fn square(value: int) -> int {
value * value
}
fn main() {
let worker = std.isolate.spawn("square", [12])
println(std.isolate.join(worker))
}Arguments and results are copied across the isolate boundary. Closures, futures, and pointers cannot cross it. This means isolates cannot accidentally race over an ordinary mutable collection.
Fanning work out: std.parallel
std.parallel is the convenient interface for applying a top-level function across a collection on multiple cores:
fn squares(values: [int]) -> [int] {
[value * value for value in values]
}
fn main() {
let values = [1, 2, 3, 4, 5, 6]
println(std.parallel.chunked("squares", values, 2))
}map(entry, items)invokes the function once per item and preserves item order in the result.chunked(entry, items, lanes)divides the input into larger contiguous chunks. Prefer it when each individual item is small.each(entry, items)runs work for effects and waits for every item.workers(entry, count)starts long-lived numbered workers.cpu_count()reports the available parallelism.
The entry point must be a named top-level function rather than a closure.
Shared read-only data
When every isolate needs the same large read-only string, freeze it before starting workers:
fn main() {
std.shared.put("dictionary", std.fs.read_file("words.txt"))
std.parallel.workers("worker", 0)
}Workers read it with std.shared.get. Treat shared entries as process-lifetime configuration or assets, not as a place for per-request mutable state.
Which should I use?
- Waiting on several timers, requests, files, or connections: async tasks.
- Passing a stream of values between async tasks: channels.
- Processing CPU-heavy data on several cores:
std.parallel. - One custom parallel job with explicit arguments and result: an isolate.
- Large immutable configuration needed by every isolate:
std.shared.
The key distinction is simple: async overlaps waiting; isolates and std.parallel use additional cores.