std/sync
Channel, wait-group, once, and atomic primitives for coordinating green tasks and Promise.blocking workers.
from "std/sync" import { Channel, WaitGroup, Once, AtomicI64, AtomicI32, AtomicU64, AtomicBool }There is no Mutex or RwLock for you to hold. The locking still happens: Channel is a shared bounded queue over a pthread mutex and two condition variables, written once here and audited behind unsafe impl Send/Sync, so producers and consumers coordinate without a lock at your call sites. What crosses the boundary is ownership, since send moves a value in and recv moves it out. Channel<T> is Send only when T is.
When several cores transform one large buffer, divide its ownership with std/shard rather than queueing the elements. See Concurrency.
Every type here is a reference-counted handle. .clone() gives another task or worker its own owner, and the shared object frees itself when the last owner drops — there is no destroy.
Every atomic operation on every type below is sequentially consistent (seq_cst), including both the success and failure orderings of a cas. There is no ordering parameter and no acquire/release/relaxed form. add/sub wrap on overflow, unlike ordinary Milo arithmetic, which traps.
Example: Lazy static
A module-level global whose initializer runs code is already initialized lazily, on its first read and exactly once, so a lazy static needs no Once at all. Reach for Once when the one-time work is a side effect at a point you choose, or fills several globals together. The shape is a global plus a guard function, because a getter cannot hand back a &T:
from "std/sync" import { Once }
var gTable: Vec<i64> = []
var gTableOnce: Once = Once.new()
pub fn ensureTable(): void {
gTableOnce.run((): void => {
gTable = buildTable()
})
}Callers do ensureTable() and then read gTable directly. There is no Lazy<T> or OnceCell<T>: with no way to return a reference, every get() would deep-copy the cached value.
Example: Producer-Consumer
The producer runs on a Promise.blocking worker so it makes progress while main consumes on the channel (a green producer would only run while the scheduler is driven):
from "std/runtime" import { Promise }
from "std/sync" import { Channel }
fn main(): i32 {
var ch = Channel<i64>.new(8)!
let producer = Promise<i64>.blocking(move (): i64 => {
ch.send(10)!
ch.send(20)!
ch.send(30)!
ch.close()
return 0
})
for val in ch {
print(val)
}
producer.await()!
print("done")
return 0
}API reference
AtomicBool
pub struct AtomicBoolLock-free atomic boolean. There is no AtomicPtr: a safe-looking one would be a lifetime claim nothing checks. Share a Vec index or an arena Handle instead.
AtomicBool.cas
fn AtomicBool.cas(self: &AtomicBool, expected: bool, desired: bool): boolReturns the value that was there — equal to expected exactly when the swap happened, which is how a caller claims a one-shot flag: f.cas(false, true) == false.
AtomicBool.clone
fn AtomicBool.clone(self: &AtomicBool): AtomicBoolShare this atomic with another owner; freed when the last owner drops.
AtomicBool.load
fn AtomicBool.load(self: &AtomicBool): boolAtomic read.
AtomicBool.new
fn AtomicBool.new(initial: bool): AtomicBoolAn atomic holding initial.
AtomicBool.store
fn AtomicBool.store(self: &AtomicBool, val: bool): voidAtomic write.
AtomicBool.swap
fn AtomicBool.swap(self: &AtomicBool, val: bool): boolAtomic swap. Returns the old value.
AtomicI32
pub struct AtomicI32Lock-free signed 32-bit atomic integer.
AtomicI32.add
fn AtomicI32.add(self: &AtomicI32, val: i32): i32Returns the OLD value. Wraps on overflow.
AtomicI32.cas
fn AtomicI32.cas(self: &AtomicI32, expected: i32, desired: i32): i32Store desired only if the current value is expected. Returns the value that was there — equal to expected exactly when the swap happened.
AtomicI32.clone
fn AtomicI32.clone(self: &AtomicI32): AtomicI32Share this atomic with another owner; freed when the last owner drops.
AtomicI32.load
fn AtomicI32.load(self: &AtomicI32): i32Atomic read.
AtomicI32.new
fn AtomicI32.new(initial: i32): AtomicI32An atomic holding initial.
AtomicI32.store
fn AtomicI32.store(self: &AtomicI32, val: i32): voidAtomic write.
AtomicI32.sub
fn AtomicI32.sub(self: &AtomicI32, val: i32): i32Returns the OLD value. Wraps on underflow.
AtomicI32.swap
fn AtomicI32.swap(self: &AtomicI32, val: i32): i32Atomic swap. Returns the old value.
AtomicI64
pub struct AtomicI64Lock-free signed 64-bit atomic integer.
AtomicI64.add
fn AtomicI64.add(self: &AtomicI64, val: i64): i64Atomic add. Returns the OLD value. Wraps on overflow.
AtomicI64.cas
fn AtomicI64.cas(self: &AtomicI64, expected: i64, desired: i64): i64Store desired only if the current value is expected. Returns the value that was there — equal to expected exactly when the swap happened.
AtomicI64.clone
fn AtomicI64.clone(self: &AtomicI64): AtomicI64Share this atomic with another owner (e.g. a spawned task). Each clone must be dropped exactly once; the underlying storage is freed when the last owner drops.
AtomicI64.load
fn AtomicI64.load(self: &AtomicI64): i64Atomic read.
AtomicI64.new
fn AtomicI64.new(initial: i64): AtomicI64An atomic holding initial.
AtomicI64.store
fn AtomicI64.store(self: &AtomicI64, val: i64): voidAtomic write.
AtomicI64.sub
fn AtomicI64.sub(self: &AtomicI64, val: i64): i64Atomic subtract. Returns the OLD value. Wraps on underflow.
AtomicI64.swap
fn AtomicI64.swap(self: &AtomicI64, val: i64): i64Atomic swap. Returns the old value.
AtomicU64
pub struct AtomicU64Lock-free unsigned 64-bit atomic integer. Uses the same instructions as AtomicI64 (64-bit atomics have no notion of sign), so add/sub wrap through the full u64 range.
AtomicU64.add
fn AtomicU64.add(self: &AtomicU64, val: u64): u64Returns the OLD value. Wraps on overflow.
AtomicU64.cas
fn AtomicU64.cas(self: &AtomicU64, expected: u64, desired: u64): u64Store desired only if the current value is expected. Returns the value that was there — equal to expected exactly when the swap happened.
AtomicU64.clone
fn AtomicU64.clone(self: &AtomicU64): AtomicU64Share this atomic with another owner; freed when the last owner drops.
AtomicU64.load
fn AtomicU64.load(self: &AtomicU64): u64Atomic read.
AtomicU64.new
fn AtomicU64.new(initial: u64): AtomicU64An atomic holding initial.
AtomicU64.store
fn AtomicU64.store(self: &AtomicU64, val: u64): voidAtomic write.
AtomicU64.sub
fn AtomicU64.sub(self: &AtomicU64, val: u64): u64Returns the OLD value. Wraps on underflow.
AtomicU64.swap
fn AtomicU64.swap(self: &AtomicU64, val: u64): u64Atomic swap. Returns the old value.
Channel
pub struct Channel<T>Bounded FIFO channel for streaming values between green tasks and Promise.blocking workers. send blocks when full, recv when empty; for v in ch drains it until close.
Channel.clone
fn Channel.clone(self: &Channel): Channel<T>Share this channel with another owner (a spawned producer/consumer). Each clone must be dropped exactly once; the queue is torn down when the last owner drops, and any payload still queued at that point is destroyed (its Drop runs once). Values already received are the receiver's and are not touched. send/recv take &Self, so a handle only needs cloning when moved into a task while the parent still uses it.
Channel.close
fn Channel.close(self: &Channel): voidSignal no more values will be sent. Pending items are still delivered.
Channel.len
fn Channel.len(self: &Channel): i64Number of values currently queued.
Channel.new
fn Channel.new(capacity: i64): Result<Channel<T>>A channel holding at most capacity values.
Channel.next
fn Channel.next(self: &mut Channel): Option<T>Iterator protocol — enables for val in channel { ... } Uses match, not let-else: std must stay within the subset milo-self parses (src-milo has no let-else yet), or self-host can't compile std.
Channel.rawPtr
fn Channel.rawPtr(self: &Channel): *u8Raw ChannelInner pointer, for std/select arm hooks (channelArm*).
Channel.recv
fn Channel.recv(self: &Channel): Result<T>Receive a value, blocking while the channel is empty.
Channel.retainHandle
fn Channel.retainHandle(self: &Channel): ChannelHandleA T-erased owner of the queue, for std/select: a channel arm keeps one so the channel outlives the arm even when every Channel<T> is dropped before wait(). If this handle ends up the last owner, the queue is freed but payloads still in it are not destroyed (only Channel<T>.drop knows T): they leak, they never dangle.
Channel.send
fn Channel.send(self: &Channel, val: T): Result<i32>Send val, blocking while the channel is full.
Channel.tryRecv
fn Channel.tryRecv(self: &Channel): Option<T>Receive without blocking: None if the channel is empty.
Channel.trySend
fn Channel.trySend(self: &Channel, val: T): boolSend without blocking: false if the channel is full.
ChannelHandle
pub struct ChannelHandleNon-generic reference-counted owner of a ChannelInner. Channel<T> holds one of these; the handle's Drop is the fallback for a handle that was never wrapped, and the refcount/free logic lives here once, free of T.
ChannelHandle.retain
fn ChannelHandle.retain(self: &ChannelHandle): ChannelHandleChannelInner
pub struct ChannelInnerBounded FIFO channel for safe message passing between threads. Channel is a reference-counted handle — .clone() to share it with another owner (a task/worker); the queue frees itself when the last owner drops. No manual destroy. Payloads still queued when the last owner drops are destroyed (their Drop runs, once each); payloads already received belong to the receiver and are untouched.
let ch = Channel<i64>.new(16)! let chW = ch.clone() let p = Promise<i64>.blocking(move (): i64 => { chW.send(42)! return 0 }) let val = ch.recv()! p.await()! // ch (and chW, dropped by the worker) free the queue automatically
Fields:
mtx: *u8condNotEmpty: *u8condNotFull: *u8buf: *u8capacity: i64len: i64head: i64tail: i64closed: i64recvWaitHead: *u8: intrusive lists of parked green tasks; nodes live on the parked task's stack: [0..8) task ptr, [8..16) next ptr. pthread waiters use the conds.sendWaitHead: *u8refcount: i64: inlined Arc-style refcount.Channel<T>.droptakes the last-owner decrement itself (it must destroy queued payloads, which needs T) and then zeroes its ChannelHandle so the handle's own Drop below is a no-op.
Once
pub struct OnceRun-exactly-once initialization guard, correct under green tasks and Promise.blocking threads alike: a green waiter parks, an OS-thread waiter blocks on a condition variable, and the main thread with a live scheduler drives it.
Once.clone
fn Once.clone(self: &Once): OnceShare this Once with another owner (a worker task/thread); freed when the last owner drops. run takes &Self, so a module-level Once never needs a clone.
Once.isDone
fn Once.isDone(self: &Once): boolTrue once the initializer has completed. Still false while it is running, so this is a progress hint, never a substitute for run.
Once.new
fn Once.new(): OnceA guard whose initializer has not run yet.
Once.run
fn Once.run(self: &Once, f: () => void): voidRun f if nobody has yet, otherwise block until whoever did is finished. Returns only once the initializer has completed exactly once, process-wide. @synchronized: f is a critical section. The embedded mutex serializes it and the atomic state word publishes its writes, so globals mutated in here are not racing.
Re-entering run from inside its own initializer would wait for itself forever, so it aborts with that message instead of hanging.
WaitGroup
pub struct WaitGroupCounting barrier: add before spawning, done from each task, wait until the counter reaches zero. Works across green tasks and OS threads alike.
WaitGroup.add
fn WaitGroup.add(self: &WaitGroup, n: i64): voidAdd n to the counter. Call it before spawning the tasks it tracks.
WaitGroup.clone
fn WaitGroup.clone(self: &WaitGroup): WaitGroupShare this WaitGroup with another owner (e.g. a worker task); freed when the last owner drops. add/done/wait take &Self, so most uses need no clone.
WaitGroup.done
fn WaitGroup.done(self: &WaitGroup): voidDecrement the counter by one. Call it from each task when it finishes.
WaitGroup.new
fn WaitGroup.new(): WaitGroupA wait group with a zero counter.
WaitGroup.wait
fn WaitGroup.wait(self: &WaitGroup): voidBlock until the counter reaches zero.