Ownership handles
API summary (generated from the Beans source by npm run coverage): 11 types · 33 instance methods.
An ownership handle is a builtin type that owns a value and controls how you reach it. Different handles give different rules: one owner, shared owners, locked access, or a value sent between threads. This page lists each handle and its methods. For the full ownership model, read the memory model. For threads and channels in context, see concurrency.
Box, Arena, Shared, and Mutex are move-only outer handles, like List and
Map. When you bind, assign, or return one, use move. Function parameters borrow
by default.
Box<T> and Arena<T> implement Send when T does. List<T> follows the
same rule; Map<K, V> and OrderedMap<K, V> require both stored types to be
Send.
Box<T>
Section titled “Box<T>”Box<T> owns one value on the heap. It is a move-only handle.
new Box(value)Box<T>.get() -> TBox<T>.set(T)new Box(value)allocates one heap slot and takes ownership ofvalue.get()returns a copy of the value;set(value)replaces it.
let b: Box<int> = new Box(10)b.set(20)let n: int = b.get()Arena<T>
Section titled “Arena<T>”Arena<T> holds many values and hands back a stable integer handle for each. It is
move-only.
new Arena(capacity)Arena<T>.add(T) -> intArena<T>.at(int) -> TArena<T>.get(int) -> Option<T>Arena<T>.len() -> intArena<T>.clear()new Arena(capacity)starts with room forcapacityitems.add(value)storesvalueand returns its handle.at(handle)reads by handle and panics if the handle is bad;get(handle)reads by handle and returnsnoneinstead of panicking.len()is the number of items;clear()removes them all.
Shared<T>
Section titled “Shared<T>”Shared<T> gives thread-safe shared ownership. Many owners can hold the same
value; the value lives until the last owner is gone. Copying the handle adds an
owner.
new Shared(value)Shared<T>.get() -> TShared<T>.downgrade() -> Weak<T>get()returns a copy of the value.downgrade()makes aWeak<T>that does not keep the value alive.
Shared<T> is Send and Sync only when T is both.
Weak<T>
Section titled “Weak<T>”Weak<T> points at a Shared<T> value without keeping it alive. Use it to break
reference cycles.
Weak<T>.upgrade() -> Option<Shared<T>>Weak<T>.is_expired() -> boolupgrade()returns a real owner again, ornoneif the value is already gone.is_expired()is true once the value is gone.
let s: Shared<int> = new Shared(1)let w: Weak<int> = s.downgrade()match w.upgrade() { some(owner) => io.println("{owner.get()}"), none => io.println("gone"),}Mutex<T>
Section titled “Mutex<T>”Mutex<T> guards a value so only one thread touches it at a time. It is move-only.
new Mutex(value)Mutex<T>.with_lock(fn(T) -> unit)with_locktakes the lock, runs your function on the value, then releases the lock. The lock is held only for the length of the call.
let m: Mutex<int> = new Mutex(0)m.with_lock(fn(v: int) -> unit { io.println("locked value is {v}")})Channel<T>
Section titled “Channel<T>”Channel<T> sends values between threads. It holds up to capacity items.
new Channel(capacity)Channel<T>.send(T)Channel<T>.receive() -> Option<T>Channel<T>.try_send(T) -> boolChannel<T>.try_receive() -> Option<T>Channel<T>.close()send(x)puts a value in.receive()takes a value out; it returnsnoneonce the channel is closed and empty.try_send(x)answers immediately:truewhen the value went in,falsewhen the channel is full or closed. It is not available for a move-onlyT, whose value a refused send could not hand back.try_receive()answers immediately:some(v), ornonewhen nothing is waiting.close()says no more values will be sent.
On a fiber, send and receive park instead of blocking the worker. See
Fibers and brew.
Thread<T>
Section titled “Thread<T>”Thread<T> is a running thread. You do not make one with new; you get it from
thread.spawn (see concurrency and the
standard library).
Thread<T>.join() -> TThread<T>.detach()join()waits for the thread to finish and returns its result.detach()lets the worker finish on its own and discards its result. Use it only when no later work depends on completion.
Brew<T>
Section titled “Brew<T>”Brew<T> is a running fiber — a green thread. You do not make one with new;
you get it from brew (see Fibers and brew). It is move-only
and scope-bound: the handle cannot be stored in a field, captured, returned, or
sent to another thread.
Brew<T>.join() -> Result<T>Brew<T>.cancel()join()parks the caller until the child finishes and answersok(value), or anerrof kindpanicwhen the child panicked,cancelledwhen it was cancelled, orclosedon a second join.cancel()requests cancellation and returns immediately. The request is observed at the child’s next park.
Cancellation cleanup is available from v0.1.51; release v0.1.50 abandons cancelled frames on all targets.
An ordinary scope exit joins every child not explicitly joined. Cancellation cleanup preserves this ownership on ELF/Mach-O x86_64 and arm64; other targets still abandon cancelled frames and do not guarantee child cleanup.
TaskGroup<T>
Section titled “TaskGroup<T>”TaskGroup<T> manages a group of child fibers whose count is a runtime value.
It is move-only and carries the same scope-bound rules as Brew<T>.
new TaskGroup<T>()TaskGroup<T>.next() -> Option<Result<T>>TaskGroup<T>.try_next() -> Option<Result<T>>TaskGroup<T>.wait_all() -> Result<List<T>>TaskGroup<T>.cancel_all()group.brew(f(x))starts a child in the group, at any block depth.next()parks for the earliest unclaimed completion and answersnoneonce every result has been collected. Results arrive in completion order.try_next()returns immediately, withnonewhen no result is ready.wait_all()joins the rest and answers a list in spawn order, or the first failure in spawn order.cancel_all()cancels newest-first, joins, and discards every outcome.
A drained group is reusable.
Gate is a sticky broadcast flag. One open() wakes every parked waiter, and
the gate stays open for every later wait(). Unlike the other fiber handles it
is Send, so an OS thread can open a gate that fibers are parked on.
new Gate()Gate.wait()Gate.open()Gate.is_open() -> boolwait()parks until the gate opens, and returns at once if it is already open.open()opens the gate and wakes the waiting line in FIFO order.is_open()reads the flag without parking.
AtomicInt
Section titled “AtomicInt”AtomicInt is a single integer that many threads can update safely. Its
operations are sequentially consistent.
new AtomicInt(0)AtomicInt.load() -> intAtomicInt.store(int)AtomicInt.add_and_get(int) -> intnew AtomicInt(0)starts the counter at a value.load()reads it;store(v)writes it.add_and_get(v)addsvand returns the new value.
let count: AtomicInt = new AtomicInt(0)let now: int = count.add_and_get(1)Handles working together
Section titled “Handles working together”A counter guarded across two threads, joined with Thread.join:
import std.ioimport std.thread
fn main() { let counter: AtomicInt = new AtomicInt(0)
let a: Thread<int> = thread.spawn(fn() -> int { var i: int = 0 for i < 1000 { counter.add_and_get(1) i += 1 } return 0 }) let b: Thread<int> = thread.spawn(fn() -> int { var i: int = 0 for i < 1000 { counter.add_and_get(1) i += 1 } return 0 }) a.join() b.join()
io.println("counted {counter.load()}")}See also
Section titled “See also”- The memory model, ownership, move, and borrowing.
- Concurrency, threads, channels, and locks in use.
- Fibers and brew,
Brew,TaskGroup, andGatein use. - Atomics,
Atomic<T>andMemoryOrder.