Skip to content
velt/sync exports

velt/sync

API reference

Lock-free shared state: the prelude's Atomic<T>, the primitive, and velt/sync (import { Backoff, VersionLatch } from "velt/sync"), building blocks written on it. They are for code that needs to be as fast as threads allow: counters, flags, statistics, and data structures that many threads read and change at once without a lock, such as a concurrent tree with a latch in every node. For a walkthrough, read the guide.

import { channel, ChannelClosed } from "velt/sync";

Exports

Re-exported from velt/sync/channel

function channel<T>(capacity: i64 = 0): Channel<T>

A new channel of T values: unbounded with capacity 0, else holding at most capacity values. Panics if capacity is negative.

class

ChannelClosed

#std/sync/errors.vlt:4

Re-exported from velt/sync/errors

class ChannelClosed extends Error

Thrown by send on a closed channel.

constructor()No doc comment yet

Re-exported from velt/sync/atom

struct Atom<T>

A shared value that threads read at once without a lock and replace whole, like Rust's arc-swap: Atom.create(shared(config)). Readers never wait and never write; a value it replaces is dropped once no thread can still be reading it (velt/sync/epoch).

  • read((v) => …) runs the callback on the current value, borrowed: no count, the cheapest read. load() returns it as a reference of its own, to keep.

  • store(v) and swap(v) replace it (swap returns the value before).

  • update((v) => …) replaces it with what the callback makes of it. Updates take turns, so the callback runs once per update, unless a store or swap came in between (then it runs again on the newer value); if it throws, nothing changes. It must not update the same Atom.

Keep it in a shared object for threads to share it, or use an Atomic<shared<T> | null> at module level.

static create(value: shared<T>): Atom<T>

An Atom holding value.

read<R, E>(f: (value: shared<T>) => R throws E): R throws E

The current value, borrowed by f: reading it there costs no count, and whatever f keeps of it (returns, stores, captures) takes a reference of its own.

load(): shared<T>

The current value, as a reference of its own.

store(value: shared<T>): void

Makes value the current value.

swap(value: shared<T>): shared<T>

Makes value the current value; returns the one before.

update<E>(f: (current: shared<T>) => shared<T> throws E): shared<T> throws E

Replaces the current value with f(current); returns the new value. Updates take turns, so f must not call update on the same Atom: it would wait for itself forever.

Re-exported from velt/sync/backoff

Waiting for another thread in a loop, twice as long each round: snooze() spins 2^round CPU spin hints up to 2^SPIN_ROUNDS, then yields the thread to the OS each round. Start a fresh one for each wait: const b = Backoff.create(); while (!ready()) b.snooze();. It waits for another thread, never for a task: whatever it waits for must be taken and let go between two awaits (use a Mutex or a channel to wait for a task).

static create(): Backoff

A backoff that has not waited yet.

spin(): void

Spins for this round, and makes the next one twice as long, up to 2^SPIN_ROUNDS.

snooze(): void

spin(), then, once the spins are as long as they get, also yields the thread.

get completed(): bool

Whether spinning stopped growing: a loop that waits longer should block instead.

Re-exported from velt/sync/channel

struct Channel<T> implements AsyncIterable<T>

A channel of T values. channel<T>() is unbounded; channel<T>(capacity) holds at most capacity values (at least 1).

async send(value: T): Promise<void> throws ChannelClosed

Queues value, waiting while a bounded channel is full. Throws ChannelClosed (and drops value) if the channel is closed, also while waiting.

trySend(value: T): bool

Queues value if the channel has room now, without waiting: true if it was queued, false (and value dropped) if a bounded channel is full or the channel is closed. Synchronous, so it may be called inside Mutex.with.

async receive(): Promise<T | null>

The oldest value, waiting for one; null once the channel is closed and empty.

tryReceive(): T | null

The oldest value if one is queued, else null (never waits).

async * [Symbol.asyncIterator](): AsyncIterator<T>

for await (const v of ch): receives until the channel is closed and drained. Leaving the loop early does not close the channel (other tasks may receive from it).

close()

Closes the channel (closing twice does nothing). Pending and later sends throw ChannelClosed; receivers drain what is queued, then get null.

get closed(): bool

Whether the channel is closed.

get length(): i64

Number of queued values.

Re-exported from velt/sync/version_latch

A node's latch word for optimistic lock coupling (bbtree's NodeLatch), 8 bytes, kept in place in a node or an array (VersionLatch[]). From the low bit: an exclusive bit, a count of shared holders, and a version that only grows.

  • Optimistically: a reader takes readVersion(), reads the node while writers may change it, then checks isUnchanged(v); if that fails it reads again. A reader writes nothing.

  • Exclusively: a writer takes the node at the version it read with tryLockAt(v) (never waiting; start over when it fails), or with lockExclusive(), and lets go with unlockExclusive(v), which moves it to the next version so readers see it changed.

  • Shared: lockShared() / unlockShared() keep writers out without changing the version.

Values read optimistically may be torn until isUnchanged says otherwise. Hold the latch only between awaits: its waits spin, so a task holding it across an await can hang the others.

static create(): VersionLatch

An unheld latch at version 0.

readVersion(): u64

The version, to read the node optimistically, once no thread holds it exclusively.

tryReadVersion(): u64 | null

The version if no thread holds the node exclusively now, else null, without waiting.

isUnchanged(version: u64): bool

Whether the node is still at version: nothing took it exclusively since, so what this thread read of it since readVersion() is what it held. Call it after the reads.

tryLockAt(version: u64): bool

Takes the node exclusively if it is still at version with no shared holder; false at once otherwise. Let go with unlockExclusive(version).

tryLockExclusive(): u64 | null

Takes the node exclusively if no thread holds it at all; its version, or null.

lockExclusive(): u64

Takes the node exclusively once no thread holds it; returns the version to let go at.

unlockExclusive(version: u64): void

Lets go of the node, held exclusively since version, and moves it to the next version.

lockShared(): void

Takes the node shared once no thread holds it exclusively.

unlockShared(): void

Lets go of the node, held shared.

const

SPIN_ROUNDS

#std/sync/backoff.vlt:47

Re-exported from velt/sync/backoff

const SPIN_ROUNDS: u32 = 10

Rounds of Backoff that spin before it yields: 2^10 spin hints is a few microseconds.