Skip to content

std.sync

std.sync provides the current synchronization floor for Janus :service and :cluster work.

There are two layers:

  • std.sync.mod keeps the pthread-backed compatibility primitives and exports SpinLock / SpinMutex.
  • Leaf modules expose the Janus-native surface: atomics, parker, Mutex[T], bounded channels, mailboxes, and cancellation.

Import Janus-native primitives from their leaf modules:

use std.sync.atomics.mod as atomics
use std.sync.parker as parker
use std.sync.mutex as mtx
use std.sync.chan as chan
use std.sync.mailbox as mb
use std.sync.cancel as cancel

Do not rely on re-exports from std.sync.mod for the leaf modules yet. Those re-exports are deferred until the current cross-module Atomic[T] layout gap is closed.

ModulePrimitiveStatus
std.sync.atomics.modAtomic[T], ordering-checked atomic opsLive
std.sync.parkerpermit-model Parker over Linux futexesLive
std.sync.mutexguard-based Mutex[T]Live, single-thread smoke covered
std.sync.chanbounded SPSC Chan[T]Live
std.sync.mailboxactor-shaped Mailbox[Msg] wrapperLive
std.sync.cancelone-shot CancelTokenLive
std.sync.modpthread Mutex, RWLock, CondvarCompatibility layer
std.sync.modSpinLock, SpinMutexLive

Atomic[T] is the typed wrapper over the SPEC-059 atomic intrinsic substrate. It gates T through the compiler-owned AtomicEligible trait and validates ordering choices at monomorphization.

use std.sync.atomics.mod as atomics
var flag = atomics.new[u32](0)
atomics.store[u32, .release](&flag, 1)
let seen = atomics.load[u32, .acquire](&flag)

Use Atomic[u32] for flags today. Atomic[bool] is deferred because the current LLVM path rejects i1 atomics.

Parker is the wait/wake substrate used by Janus-native blocking primitives.

use std.sync.parker as parker
var p = parker.parker_new()
parker.parker_unpark(&p)
parker.parker_park(&p)

The contract is permit-based:

  • parker_unpark deposits a permit.
  • parker_park consumes an existing permit or blocks until one arrives.
  • parker_park_timeout returns Unparked or TimedOut.

The current backend is Linux futexes. NexusOS backend selection is future work.

std.sync.mutex.Mutex[T] protects a value and returns a guard.

use std.sync.mutex as mtx
var mu = mtx.mutex_new[u32](41)
var guard = mtx.mutex_lock[u32](&mu)
let value = mtx.guard_value_of[u32](&guard)
value.* = value.* + 1
mtx.mutex_release[u32](&guard)

The mutex uses a three-state futex-style algorithm:

  • 0: unlocked
  • 1: locked without known waiters
  • 2: locked with possible waiters

Keep the mutex address stable while any thread may park on it. Moving a parker-backed type breaks the futex address contract until Pin / move-on-drop support lands.

Chan[T] is a bounded single-producer, single-consumer ring buffer over caller-owned backing storage.

use std.sync.chan as chan
var backing: [4]u32 = .undefined
var c = chan.chan_new[u32](backing[..])
let sent = chan.chan_send[u32](&c, 42)
if sent != chan.ChanError.Ok do
return
end
let got = chan.chan_recv[u32](&c)

Important semantics:

  • FIFO ordering.
  • Bounded capacity from the backing slice.
  • Close is idempotent.
  • Receivers drain buffered values before closed state becomes terminal.
  • chan_recv returns null only when the channel is closed and drained.

Struct payloads work directly through chan_new — no special constructor:

pub struct Msg { kind: u32, value: u32 }
var slots: [8]Msg = .undefined
var c = chan.chan_new[Msg](slots[..])

Both fields round-trip per-call across multiple sends. (The earlier chan_new_with_cap[T] workaround was retired once the cross-module generic struct-by-value path closed — see the v2026.5.15 release note.)

chan_recv_timeout waits for a relative nanosecond duration:

let msg = chan.chan_recv_timeout[u32](&c, 1_000_000)
if msg == null do
// timeout, or closed-and-drained
end

chan_recv_cancellable also observes a CancelToken:

use std.sync.cancel as cancel
var tok = cancel.cancel_token_new()
let msg = chan.chan_recv_cancellable[u32](&c, &tok)

The receive helpers return ?T. null merges timeout, cancellation, and closed-drained. If a caller needs the cause, check chan_is_closed or cancel_token_is_cancelled after return.

Mailbox[Msg] wraps Chan[Msg] with actor-oriented names.

use std.sync.mailbox as mb
pub struct Cmd { kind: u32, payload: u32 }
var slots: [16]Cmd = .undefined
var box = mb.mailbox_new[Cmd](slots[..])
_ = mb.mailbox_send[Cmd](&box, Cmd{ kind: 1, payload: 42 })
let got = mb.mailbox_recv[Cmd](&box)

Struct messages preserve every field per-call. chan_smoke scenario 9 and test-mailbox-struct-payload lock this end-to-end.

The current mailbox inherits the channel’s SPSC contract. Multi-producer fan-in is not part of the shipped surface yet.

CancelToken is a one-shot, idempotent cancellation signal.

use std.sync.cancel as cancel
var tok = cancel.cancel_token_new()
cancel.cancel_token_cancel(&tok)
if cancel.cancel_token_is_cancelled(&tok) do
return
end

For channel or mailbox consumers, cancellation code should usually signal the token and close the queue so blocked receivers wake promptly:

cancel.cancel_token_cancel(&tok)
mb.mailbox_close[Cmd](&box)
  • Chan[T] and Mailbox[Msg] are SPSC only.
  • Multi-thread proof coverage is still narrower than the API shape.
  • Leaf-module re-exports from std.sync.mod are deferred.
  • Atomic[bool] and Mutex[bool] are deferred.
  • NexusOS parker backend is deferred.