Concurrency and Threads
One thread is easy to reason about and slow at scale. Rasmalai threads share memory under reference counting, coordinate through a few small primitives, and offer cooperative tasks for the waits in between. None of it needs unsafe.
Spawning threads
Thread.spawn takes a zero-arg non-throwing function or closure and returns a handle; handle.join() blocks and returns a Result — Ok with the worker value, Err with a message when the worker fails. Await each handle once; worker results are Int/Bool/Float/String, and anything else is an E108 at spawn:
let factor = 2;
let handle = Thread.spawn(() => 21 * factor);
let res = handle.join();
print(res.unwrap());Handles have a nameable type: import { Thread } from "@std/sync" and store them in an Array<Thread>, then join each element. Spawning inside the loop and joining immediately would serialize the workers, so spawn every handle first and join afterwards:
import { Thread } from "@std/sync";
fn worker(): Int {
return 1;
}
let workers: Array<Thread> = [];
let i = 0;
while i < 16 {
workers.push(Thread.spawn(worker));
i = i + 1;
}
for t in workers {
t.join();
}A barrier only trips when count parties are alive and waiting at the same time. Spawning one worker and joining it in the same loop iteration deadlocks a multi-party barrier: each iteration blocks in join() with a single worker alive, so the gate never fills and the program hangs with no diagnostic. Spawn every handle first, join afterwards — and size count to the number of concurrently running parties, never the number of loop iterations.
The ownership rule is load-bearing, not stylistic: the parent's objects can drop while the worker runs, so anything the worker needs must be computed from values it owns. A spawned closure captures its environment by reference, and the compiler does not stop a shared object from crossing threads — concurrent access to a shared object is the programmer's responsibility. Values that are safe to share are integers, @std/sync primitives built byId, and the internally locked Map/Set; a plain class with mutable fields is not: two threads bumping one counter lose updates to a read-modify-write race, with a final count that varies run to run and falls short of the sum.
Sharing through byId primitives
When threads must share, they share through the @std/sync primitives, all constructed byId so separate threads open the same cell. An AtomicInt counts without locks; a Channel moves values between threads (send accepts Int, Float, String, Array, or any object, and recv returns Any, so the receiver casts before use); Mutex (lock/unlock/tryLock), RwLock (shared read or exclusive write locks), Condvar (wait with its mutex held, notifyOne/notifyAll), and Barrier (byId(id, count) with count greater than zero — zero or negative counts fail immediately — plus wait, which parks every party until the last one arrives, returns true for exactly one leader, and resets for the next round) cover the rest:
import { AtomicInt } from "@std/sync";
let c = AtomicInt.byId(1);
c.set(40);
print(c.fetchAdd(2), c.get());A Map filled concurrently from two threads, collected with join:
import { Map } from "@std/collections";
fn fill(totals: Map, base: Int): Int {
for j in 0 .. 1000 {
totals.set(base + j, j);
}
return 1;
}
fn Main(): Int {
let totals = new Map<Int, Int>();
let a = Thread.spawn((): Int => fill(totals, 0));
let b = Thread.spawn((): Int => fill(totals, 1000));
print("a=${a.join().unwrap()} b=${b.join().unwrap()}");
print("len=${totals.len()} (expected 2000)");
return 0;
}Channel.close releases anything still queued, and the channel retains sent heap values, transferring the reference to the receiver. Every primitive has a generated reference page under its module docs.
Pools for data parallelism
ThreadPool is compiler-recognized like Thread — no import needed. new(workers) creates a pool (or byId(id, workers) for a named one), submit(task) queues a zero-arg function or closure and returns a task handle, submitArg(task, arg) queues a one-Int-arg function or closure, and parallelFor(start, end, chunk, worker) runs a worker over every index in chunks and blocks until done, with join and shutdown for lifecycle control. task.join() blocks exactly once per handle and returns that task's Result:
let pool = ThreadPool.new(4);
let task = pool.submit(() => "done".concat("!"));
let out = task.join();
print(out.unwrap());
pool.shutdown();Retains and releases are atomic across threads, so an object shared through a pool drops deterministically on whichever thread releases it last — with no pause and no finalizer queue.
The filesystem pool follows the same shape with no per-call setup: one process-wide pool serves every @std/fs *Async call, sized from the CPU count and clamped into 1..8. fs.setWorkers(n) resizes it (clamped the same way) but only while no async operation is in flight — resizing stops the old pool, so anything still queued never runs. Calls queue work instead of spawning a thread each, and a nested *Async issued from inside a pool worker runs inline instead of queuing, so a full pool can never deadlock waiting on itself. Plain sync calls never touch the pool; they block the caller directly.
Cooperative tasks with async
async fn foo(): T returns a Promise<T>; await child() parks the worker thread at 0% CPU until the child settles, then resumes with the fulfilled value (rejection propagates and rejects the awaiting function in turn). No hidden executor, no polling: direct .poll() calls are an E111 error and the Poll enum is retired. pass is the explicit no-op for empty cases.
async fn compute(): Int {
return 41 + 1;
}
let result = await compute();
return result;Main itself may be async; the compiler runs it and uses the resolved value as the exit code. Or skip the wrapper: the entry file runs top-level statements directly, so top-level await just works. From synchronous code, bridge with a blocking wait instead: compute().wait().unwrap(). await outside an async fn is an E109 error, except at the top level of the entry file.
Summary
Thread.spawn+join()returningResult; await each handle once.@std/syncprimitives (AtomicInt,Channel,Mutex,RwLock,Condvar,Barrier) constructedbyIdfor cross-thread sharing — the only legal sharing path.ThreadPool.new/byIdwithsubmit+join()and chunkedparallelFor.- One process-wide fs pool backs every
@std/fs*Asynccall (CPU count clamped to 1..8,setWorkersto resize, nested calls run inline). async/awaitoverPromise<T>with.wait()bridging;E109guards misplaced awaits,E111bans direct polling.