API Reference
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).
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): AtomicIntOpen 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): BoolCompare-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());fn fetchAdd(delta: Int): IntAdd 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());fn get(): IntRead 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);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.
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.
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): BarrierOpen 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());fn wait(): BoolBlock 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());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.
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): ChannelOpen 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(): VoidDrop 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(): IntQueued 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(): AnyDequeue 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());fn send(val: Any): VoidEnqueue 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());fn tryRecv(): IntDequeue 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());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.
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): CondvarOpen 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(): VoidWake 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();fn notifyOne(): VoidWake 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): VoidSleep 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();fn waitTimeout(m: Mutex, millis: Int): BoolSleep 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();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.
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): MutexOpen 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(): VoidAcquire 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();fn tryLock(): BoolAcquire 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();fn unlock(): VoidRelease 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.
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.
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): RwLockOpen 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(): VoidAcquire 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(): VoidRelease shared read access.
Wakes a waiting writer once the last reader is out. Aborts the program when no read access is held.
fn tryReadLock(): BoolTake 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();fn tryWriteLock(): BoolTake 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();fn writeLock(): VoidAcquire 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(): VoidRelease exclusive write access and wake everyone waiting on the lock.
Aborts the program when write access is not held.
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).
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): ThreadOpen 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());