@std/sync

API Reference

class AtomicInt

A 64-bit integer cell shared through the registry, with atomic access. The cell starts at 0 the first time its id is used. Every operation is sequentially consistent, so a value stored with `set` is visible to the next `get` from any thread, and `fetchAdd` and `cas` read-modify-write the cell without a lock in between. Reach for `fetchAdd` to build counters and for `cas` when a thread must only write if nobody else moved the value first. Ordinary non-atomic fields of an object are not covered by this: only the cell itself is atomic. Handles share by id, so a thread that only knows the number reaches the same cell with `AtomicInt.byId(id)`. ```rnx import { AtomicInt } from "@std/sync"; let a = AtomicInt.byId(11); a.set(41); print(a.get() + 1); ```

init(id: Int)

fields

  • id: Int
fn byId(id: Int): AtomicInt

Open the shared cell registered under id, creating it at 0 if this is the first use of that id.

id — registry slot shared with other threads. Pick an id no other code in the program uses; a number reused by a different primitive names a different object.

returns — handle to the cell.

fn cas(expected: Int, newVal: Int): Bool

Compare-and-swap: store newVal only when the cell holds expected. The comparison and the store are one indivisible step, so the call either swaps or reports that another thread got there first. When it returns false the cell is left alone, so read it again to see what the current value is.

expected — value that must be present for the swap to happen.

newVal — value to store on match.

returns — true when the swap happened.

import { AtomicInt } from "@std/sync";

let slot = AtomicInt.byId(8);
slot.set(1);
print(slot.cas(1, 2), slot.get());
print(slot.cas(1, 3), slot.get());
Run in Playground
fn fetchAdd(delta: Int): Int

Add delta to the cell and report the value that was there first. The read and the write are one indivisible step, so several threads can add to the same cell without losing updates. This is the building block for counters, unique id handouts, and work queues.

delta — amount to add. Negative values subtract.

returns — value before the add.

import { AtomicInt } from "@std/sync";

let a = AtomicInt.byId(1);
a.set(10);
print(a.fetchAdd(5), a.get());
Run in Playground
fn get(): Int

Read the cell.

returns — current value, as stored by the most recent `set` or atomic update that this thread can observe.

import { AtomicInt } from "@std/sync";

let a = AtomicInt.byId(11);
a.set(41);
print(a.get() + 1);
Run in Playground
fn set(val: Int)

Store a new value, replacing whatever was there.

val — value to store. Written in full, so a reader never sees a half-updated value.

class Barrier

Rendezvous gate: all count parties block in `wait` until the count-th party arrives, then all are released and the gate resets for the next generation. `wait` returns true for exactly one leader per generation. Construct with `byId(id, count)` where count must be greater than 0. Use it when threads have to reach the same point before any of them can continue, such as the end of a parallel phase. The gate resets itself, so one handle serves every generation without being rebuilt. Because it is a rendezvous, every party must actually arrive. A thread that exits early leaves the others parked forever, and a single thread calling `wait` on a barrier of two or more never returns. A count of 1 returns true on every call and never blocks, which is a convenient way to exercise the gate. ```rnx import { Barrier } from "@std/sync"; let solo = Barrier.byId(61, 1); print("first generation:", solo.wait()); print("gate resets:", solo.wait()); let pair = Barrier.byId(62, 2); print("two parties required:", pair.count); ```

init(id: Int, count: Int)

fields

  • count: Int
  • id: Int
fn byId(id: Int, count: Int): Barrier

Open the shared barrier registered under id. The first party to arrive sets the trip threshold, so every thread must build its handle with the same count. The gate is reusable: each trip resets it for the next generation.

id — registry slot shared with other threads. Pick an id no other code in the program uses; a number reused by a different primitive names a different object.

count — parties required to trip the gate, greater than 0. Every handle for this id should pass the same number.

returns — handle to the barrier.

throws — when count is 0 or negative, with a message naming the bad value. The throw runs in a function that is not declared `throws`, so an enclosing `try` does not intercept it: the program stops with `Uncaught exception` and exit status 1. Validate the count before calling when it comes from outside the program.

import { Barrier } from "@std/sync";

let solo = Barrier.byId(61, 1);
print("first generation:", solo.wait());
print("gate resets:", solo.wait());
Run in Playground
fn wait(): Bool

Block until count parties arrive. All count parties park; the count-th arrival releases the whole generation at once, resets the gate, and returns true for exactly one leader (false for the rest). The leader is whichever thread arrived last, so use the return value to elect one thread for per-generation work such as merging results.

returns — true for the single party whose arrival tripped the gate, false for the others. Every party sees the same generation release.

import { Barrier } from "@std/sync";

let solo = Barrier.byId(61, 1);
print("leader:", solo.wait());
Run in Playground
class Channel

An unbounded first-in-first-out queue shared through the registry. Any thread holding a handle for an id can enqueue and dequeue on the same queue. Values arrive in the order they were sent, and the queue grows to fit whatever is written to it, so a fast producer can outrun a slow consumer and use memory until it is drained. `send` takes an `Any`, and `recv` hands the value back as an `Any`, so narrow the result with an `is` check before you use it. Heap payloads (strings, arrays, objects) are retained for the receiver and released when it drops them, so a value sent across threads stays valid. Queued values are tracked per originating heap, so a value sent by one thread is delivered to whichever receiver can take ownership of it. A queue drained from a different heap than the one that filled it releases the leftovers instead of handing them out. `close` releases whatever is queued and leaves the id registered, so a later `send` on the same handle still works. It does not wake a thread already parked in `recv`: a blocked consumer stays blocked until a value arrives, so treat `close` as a way to free buffered values rather than as a shutdown signal. ```rnx import { Channel } from "@std/sync"; let jobs = Channel.byId(9); jobs.send(7); jobs.send(8); print("queued:", jobs.len()); print("took:", jobs.tryRecv()); print("next:", jobs.recv()); print("empty:", jobs.tryRecv()); ```

init(id: Int)

fields

  • id: Int
fn byId(id: Int): Channel

Open the shared queue registered under id, creating it empty on first use.

id — registry slot shared with other threads. Pick an id no other code in the program uses; a number reused by a different primitive names a different object.

returns — handle to the queue.

fn close(): Void

Drop the queue, releasing anything still queued. Queued heap payloads are freed here. The id stays registered and a later `send` on the same handle works again, so this releases buffered values without unregistering the queue. A thread parked in `recv` is not woken.

fn len(): Int

Queued message count.

returns — number of waiting messages. Advisory only: another thread may enqueue or dequeue right after the read, so do not build a decision on it that `tryRecv` has to undo.

fn recv(): Any

Dequeue the oldest value, blocking until one arrives. Parks the calling thread while the queue is empty. `close` does not release it, so a consumer waiting here needs a `send` from elsewhere, or a `Condvar` with a timeout to bound the wait.

returns — the value as Any; cast before use.

import { Channel, Thread } from "@std/sync";

fn produce(): Int {
let inbox = Channel.byId(3);
inbox.send("payload");
return 1;
}

let inbox = Channel.byId(3);
let worker = Thread.spawn(produce);
print("received:", inbox.recv());
print("worker done:", worker.join().unwrap());
Run in Playground
fn send(val: Any): Void

Enqueue a value, retaining heap payloads for the receiver. The call never blocks, whatever the queue length. Values come out in the order they went in.

val — Int, Float, String, Array, or object. A heap payload is retained on the way in, so the receiver owns it after `recv`.

import { Channel } from "@std/sync";

let jobs = Channel.byId(9);
jobs.send(7);
jobs.send(8);
print("queued:", jobs.len());
print("took:", jobs.tryRecv());
Run in Playground
fn tryRecv(): Int

Dequeue the oldest value only if one is already queued. The polling counterpart to `recv`: it reports absence in the return value instead of parking, so a loop over it never blocks.

returns — the value as Any when one was ready, or -1 when the queue was empty. Test against -1, not against a boolean, and narrow a real value with an `is` check before using it.

import { Channel } from "@std/sync";

let jobs = Channel.byId(9);
jobs.send(7);
print(jobs.tryRecv(), jobs.tryRecv());
Run in Playground
class Condvar

Condition variable for wait/notify coordination, shared through the registry. A condvar carries no state of its own. It only parks and wakes threads, so the thing being waited on has to live somewhere else, normally behind the `Mutex` handed to `wait`. The usual shape is a loop: take the mutex, test the condition, and call `wait` while the test still says "not ready". `wait` and `waitTimeout` must be called with that mutex held. The call releases the mutex while the thread sleeps and takes it again before returning, so no other thread can slip in between the test and the sleep. Calling either without holding the mutex aborts the program. ```rnx import { Condvar, Mutex } from "@std/sync"; let m = Mutex.byId(51); let c = Condvar.byId(51); m.lock(); print("woken:", c.waitTimeout(m, 30)); m.unlock(); c.notifyAll(); print("notify with no waiters is a no-op"); ```

init(id: Int)

fields

  • id: Int
fn byId(id: Int): Condvar

Open the shared condvar registered under id, with no waiters on first use.

id — registry slot shared with other threads. Pick an id no other code in the program uses; a number reused by a different primitive names a different object.

returns — handle to the condvar.

fn notifyAll(): Void

Wake every parked waiter, if any. Does nothing when no thread is waiting. Use it when several waiters share a condition and a change releases all of them at once.

import { Condvar, Mutex } from "@std/sync";

let m = Mutex.byId(51);
let c = Condvar.byId(51);
m.lock();
c.notifyAll();
m.unlock();
Run in Playground
fn notifyOne(): Void

Wake one parked waiter, if any. Does nothing when no thread is waiting, so a notify sent too early is lost. The woken thread holds its mutex again by the time `wait` returns.

fn wait(m: Mutex): Void

Sleep until notified, releasing the mutex while parked. The mutex is released before the thread sleeps and re-taken before this returns, so the caller still holds it on the way out. A notify that arrives before the sleep is missed, which is why the condition has to be re-tested in a loop after waking.

m — mutex held by the caller. The call aborts the program when it is not held.

import { Condvar, Mutex } from "@std/sync";

let m = Mutex.byId(51);
let c = Condvar.byId(51);
m.lock();
c.notifyAll();
m.unlock();
Run in Playground
fn waitTimeout(m: Mutex, millis: Int): Bool

Sleep until notified or the timeout elapses, whichever comes first. Releases and re-takes the mutex the same way `wait` does, so a notifying thread can make progress while this thread sleeps. Useful for a poll loop that must not block forever, and for giving up on a thread that never arrives.

m — mutex held by the caller. The call aborts the program when it is not held.

millis — maximum park time. A negative value is treated as 0.

returns — true when woken by a notify, false on timeout.

import { Condvar, Mutex } from "@std/sync";

let m = Mutex.byId(51);
let c = Condvar.byId(51);
m.lock();
print("woken:", c.waitTimeout(m, 30));
m.unlock();
Run in Playground
class Mutex

Mutual exclusion lock shared through the registry. One thread holds the lock at a time. The others park in `lock` until it is released, which is what makes a read-modify-write on shared state safe. The lock is not reentrant and carries no owner. A thread that already holds it and calls `lock` again parks forever, and `unlock` on a lock nobody holds aborts the program. Every `lock` needs exactly one matching `unlock` on each path out of the critical section, including error paths. For waiting on a condition rather than for the lock itself, pair this with `Condvar`, which releases the mutex while a thread sleeps. ```rnx import { Mutex } from "@std/sync"; let m = Mutex.byId(31); m.lock(); m.unlock(); print(m.tryLock()); m.unlock(); ```

init(id: Int)

fields

  • id: Int
fn byId(id: Int): Mutex

Open the shared mutex registered under id, unlocked on first use.

id — registry slot shared with other threads. Pick an id no other code in the program uses; a number reused by a different primitive names a different object.

returns — handle to the mutex.

fn lock(): Void

Acquire the lock, blocking until it is free. Parks the calling thread while another thread holds the lock. Deadlocks if the same thread already holds it, or if two threads take two locks in opposite orders.

import { Mutex } from "@std/sync";

let m = Mutex.byId(31);
m.lock();
m.unlock();
print(m.tryLock());
m.unlock();
Run in Playground
fn tryLock(): Bool

Acquire the lock only when it is free, never blocking. The polling counterpart to `lock`, for code that would rather give up a lock than wait for it. It reports the outcome in the return value, so a failed attempt leaves the lock untouched.

returns — true when the lock was taken by this call, false when it was already held. Release the lock only when this returns true.

import { Mutex } from "@std/sync";

let a = Mutex.byId(31);
let b = Mutex.byId(31);
a.lock();
print(b.tryLock());
a.unlock();
Run in Playground
fn unlock(): Void

Release a held mutex and wake one thread waiting in `lock`. Aborts the program when the lock is not held, so pair every `lock` with exactly one `unlock`.

class RwLock

Reader-writer lock shared through the registry. Many readers may hold the lock at once, or one writer may hold it exclusively, never both. Use it when a shared structure is read far more often than it changes; a plain `Mutex` is simpler and just as cheap when the critical section is short. `writeLock` waits for the last reader as well as for any writer, so keep read sections short or a writer can sit behind a stream of them. There is no way to upgrade a held read lock to a write lock: the thread would wait for its own read access to end. The lock carries no owner and is not reentrant. `readUnlock` with no reader and `writeUnlock` with no writer both abort the program. ```rnx import { RwLock } from "@std/sync"; let r = RwLock.byId(41); r.writeLock(); print("read while writer:", r.tryReadLock()); print("second writer:", r.tryWriteLock()); r.writeUnlock(); print("read when free:", r.tryReadLock()); r.readUnlock(); ```

init(id: Int)

fields

  • id: Int
fn byId(id: Int): RwLock

Open the shared rwlock registered under id, fully released on first use.

id — registry slot shared with other threads. Pick an id no other code in the program uses; a number reused by a different primitive names a different object.

returns — handle to the rwlock.

fn readLock(): Void

Acquire shared read access, blocking only while a writer holds the lock. Any number of readers hold the lock at the same time, so readers never wait for each other. Must be released with one `readUnlock`.

fn readUnlock(): Void

Release shared read access. Wakes a waiting writer once the last reader is out. Aborts the program when no read access is held.

fn tryReadLock(): Bool

Take read access only when no writer is active, never blocking. Succeeds while readers hold the lock, since readers do not exclude each other. The polling counterpart to `readLock`.

returns — true when read access was taken by this call. Release it with `readUnlock` only when this returns true.

import { RwLock } from "@std/sync";

let r = RwLock.byId(41);
r.writeLock();
print(r.tryReadLock());
r.writeUnlock();
Run in Playground
fn tryWriteLock(): Bool

Take write access only when the lock is completely free, never blocking. Fails while any reader or writer holds the lock. The polling counterpart to `writeLock`.

returns — true when write access was taken by this call. Release it with `writeUnlock` only when this returns true.

import { RwLock } from "@std/sync";

let r = RwLock.byId(41);
print(r.tryWriteLock());
r.writeUnlock();
Run in Playground
fn writeLock(): Void

Acquire exclusive write access, blocking until no reader and no writer hold the lock. Parks the calling thread for as long as any reader is active. Must be released with one `writeUnlock`.

fn writeUnlock(): Void

Release exclusive write access and wake everyone waiting on the lock. Aborts the program when write access is not held.

class Thread

Owned handle to a spawned thread. `Thread.spawn` returns this nominal type so handles can live in collections such as `Array<Thread>`. Handles created before this type existed (bare `Thread.spawn` without importing `@std/sync`) still work as raw ids with the same `join()`. `spawn` is a compiler intrinsic rather than a method here: it takes a zero-argument function that returns an `Int`, `Bool`, `Float`, `String`, or null, and the checker rejects anything else at the call site. The spawned body runs on its own native thread, so what it touches is shared only when it goes through a `byId` handle. A spawned function that throws is not a build error, the way a named `throws` function passed to `spawn` is. The throw is captured and surfaces as an `Err` from `join`. `join` waits up to 30 seconds for the thread to finish and consumes the handle, so joining the same handle twice reports the second attempt as a dead thread. A thread that has not finished by then yields `Err(thread N join timed out after 30s)`. ```rnx import { Thread } from "@std/sync"; fn worker(): Int { return 40 + 2; } let workers: Array<Thread> = []; workers.push(Thread.spawn(worker)); print(workers[0].join().unwrap()); ```

init(id: Int)

fields

  • id: Int
fn byId(id: Int): Thread

Open a handle to an existing native thread slot. `Thread.spawn` is an intrinsic and the compiler claims every `Thread.<name>` call, so this method is not reachable from Rasmalai source: the compiler answers `unknown Thread.byId` before the class body is consulted. A handle is a plain wrapper around a slot id, and the id is readable from a spawned handle's `id` field.

id — slot id returned by a previous spawn.

returns — handle to the thread.

fn join(): Result<Any, String>

Wait for the thread to finish, up to 30 seconds. Output the thread produced is merged into this thread's output when it is joined. The handle is consumed, so a second `join` on the same handle reports a dead thread.

returns — Ok(value) with the thread return, Err(message) on failure. The message is `uncaught <value>` for a body that threw, `Thread panicked` for a panic, the 30 second timeout note for a thread that ran too long, `join of dead thread N` for a reused handle, and a spawn failure note when the thread never started.

import { Thread } from "@std/sync";

fn worker(): Int {
return 40 + 2;
}
let workers: Array<Thread> = [];
workers.push(Thread.spawn(worker));
print(workers[0].join().unwrap());
Run in Playground