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.
Types
Channel
struct Channel {
h: *u8,
}Bounded FIFO channel for streaming values between green tasks and Promise.blocking workers. Blocks on send when full, blocks on recv when empty.
WaitGroup
struct WaitGroup {
_p: *u8,
}Counting barrier — add before spawning, done from each task, wait for the counter to reach zero.
Once
struct Once {
_p: *u8,
}Run-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.
AtomicI64
struct AtomicI64 {
_ptr: *u8,
}Lock-free signed 64-bit atomic integer.
AtomicI32
struct AtomicI32 {
_ptr: *u8,
}Lock-free signed 32-bit atomic integer.
AtomicU64
struct AtomicU64 {
_ptr: *u8,
}Lock-free unsigned 64-bit atomic integer. Rides the same instructions as AtomicI64 — 64-bit atomics are bit-level operations with no notion of sign — so add/sub wrap through the full u64 range.
AtomicBool
struct AtomicBool {
_ptr: *u8,
}Lock-free atomic boolean.
There is no AtomicPtr. A raw pointer is only dereferenceable inside unsafe, so an AtomicPtr would be AtomicI64 plus a cast with no safety added — and Milo cannot state that the pointee outlives the load, so a safe-looking AtomicPtr would be a lifetime claim nothing checks. Share an index into a Vec or an arena Handle instead.
Channel Methods
Channel.new
fn Channel.new(capacity: i64): Result<Channel<T>>Create a bounded channel with the given capacity.
ch.send
fn Channel.send(self: &Channel, val: T): Result<i32>Send a value into the channel. Blocks if full.
ch.recv
fn Channel.recv(self: &Channel): Result<T>Receive a value from the channel. Blocks if empty.
ch.trySend
fn Channel.trySend(self: &Channel, val: T): boolNon-blocking send. Returns true if sent, false if full.
ch.tryRecv
fn Channel.tryRecv(self: &Channel): Option<T>Non-blocking receive. Returns Option.None if empty.
ch.len
fn Channel.len(self: &Channel): i64Current number of items in the channel.
ch.clone
fn Channel.clone(self: &Channel): Channel<T>Give another owner (a producer task, a worker thread) its own handle. The queue is torn down when the last one drops.
WaitGroup Methods
WaitGroup.new
fn WaitGroup.new(): WaitGroupCreate a new wait group with a zero counter.
wg.add
fn add(self: &WaitGroup, n: i64): voidAdd n to the counter — call before spawning the tasks it tracks.
wg.done
fn done(self: &WaitGroup): voidDecrement the counter by one — call from each task when it finishes.
wg.wait
fn wait(self: &WaitGroup): voidBlock until the counter reaches zero.
wg.clone
fn clone(self: &WaitGroup): WaitGroupGive a worker its own owner. add/done/wait take &Self, so most uses need no clone.
Once Methods
Once.new
fn Once.new(): OnceCreate a guard whose initializer has not run yet.
o.run
fn 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, and every caller that returns has seen its writes.
Re-entering run from inside its own initializer would wait for itself forever; it aborts with that message rather than hanging.
o.isDone
fn 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.
o.clone
fn clone(self: &Once): OnceGive another owner its own handle. run takes &Self, so a module-level Once never needs a clone.
AtomicI64 Methods
AtomicI64.new
fn AtomicI64.new(v: i64): AtomicI64Create an atomic integer with initial value.
a.load
fn load(self: &AtomicI64): i64Atomic read.
a.store
fn store(self: &AtomicI64, v: i64): voidAtomic write.
a.add
fn add(self: &AtomicI64, v: i64): i64Atomic add, wrapping. Returns old value.
a.sub
fn sub(self: &AtomicI64, v: i64): i64Atomic subtract, wrapping. Returns old value.
a.swap
fn swap(self: &AtomicI64, v: i64): i64Atomic swap. Returns old value.
a.cas
fn cas(self: &AtomicI64, expected: i64, desired: i64): i64Compare-and-swap. Returns the value that was there — equal to expected exactly when the swap happened.
a.clone
fn clone(self: &AtomicI64): AtomicI64Give another owner its own handle; the storage frees when the last one drops.
AtomicI32 Methods
AtomicI32 carries the same surface as AtomicI64 at 32 bits: AtomicI32.new(v: i32), load, store, add, sub, swap, cas, clone.
AtomicU64 Methods
AtomicU64 carries the same surface as AtomicI64 over u64: AtomicU64.new(v: u64), load, store, add, sub, swap, cas, clone.
AtomicBool Methods
AtomicBool.new
fn AtomicBool.new(v: bool): AtomicBoolCreate an atomic boolean with initial value.
a.load
fn load(self: &AtomicBool): boolAtomic read.
a.store
fn store(self: &AtomicBool, v: bool): voidAtomic write.
a.swap
fn swap(self: &AtomicBool, v: bool): boolAtomic swap. Returns old value.
a.cas
fn cas(self: &AtomicBool, expected: bool, desired: bool): boolCompare-and-swap. Returns the value that was there, which is how a caller claims a one-shot flag: f.cas(false, true) == false.
a.clone
fn clone(self: &AtomicBool): AtomicBoolGive another owner its own handle; the storage frees when the last one drops.
Example: Lazy static
A module-level var already runs a real initializer in dependency order before main, so an eager static needs no Once at all. Reach for Once when the work must be deferred past the start of main or is expensive and usually unwanted. 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
}