Concurrency Primitives
Concurrency Primitives
Section titled “Concurrency Primitives”Complete reference for the :service profile concurrency system. All primitives described here require the :service profile or higher.
Nursery
Section titled “Nursery”A nursery is a structured concurrency scope — a bounded region where concurrent tasks live. The nursery does not exit until every task spawned inside it has completed, failed, or been cancelled. This guarantee eliminates orphan tasks by construction.
Syntax
Section titled “Syntax”nursery do // spawn tasks hereendWith a binding (for nested spawn or cancellation control):
nursery |n| do n.spawn(some_task) // n is the nursery handleendLifecycle
Section titled “Lifecycle”A nursery transitions through three states:
Open ──────► Closing ──────► Closed │ │ │ spawn() │ cancel fires or │ allowed │ last child exits │ │ │ ▼ │ No new spawns. │ Waiting for running │ children to finish. ▼Tasks run concurrently.awaitAll() blocks untilall children are done.| State | spawn allowed | await returns | Description |
|---|---|---|---|
| Open | Yes | Blocks | Nursery is active, tasks are running |
| Closing | No | Blocks | Cancel fired or all work submitted; waiting for children |
| Closed | No | Returns | All children done, nursery scope exits |
Error Propagation
Section titled “Error Propagation”When any child task fails (returns an error), the nursery enters fail-fast mode:
- The nursery’s
CancelTokenfires - All sibling tasks receive the cancellation signal at their next yield point
- The nursery waits for all fibers to complete their
defercleanup - The first error is propagated to the nursery scope’s caller
nursery do spawn task_a() // Succeeds spawn task_b() // Fails with ConnectionError spawn task_c() // Cancelled when task_b failsend catch |err| do // err == ConnectionError println("Nursery failed: ", err)endIf multiple tasks fail concurrently, the first error wins. Subsequent errors from cancelled siblings are suppressed.
Nested Nurseries
Section titled “Nested Nurseries”Tasks can create sub-nurseries for hierarchical concurrency:
nursery |root| do spawn func() do nursery |inner| do spawn subtask_a() spawn subtask_b() end endendCancellation propagates transitively: cancelling the root cancels all inner nurseries and their children.
Per-Task Nursery Stacks
Section titled “Per-Task Nursery Stacks”Each task spawned inside a nursery gets its own dedicated fiber stack. The default stack size is determined by the active profile:
| Profile | Default Stack |
|---|---|
:core | 64 KB |
:service | 256 KB |
:cluster | 256 KB |
:sovereign | 512 KB |
Override per-spawn when a task needs more (or less) stack:
nursery |n| do n.spawnWithOpts(heavy_crypto_task, .{ .stack_size = 512 * 1024 })endspawn launches a new fiber task inside the current nursery. The task runs concurrently with the spawning fiber and all other tasks in the same nursery.
Syntax
Section titled “Syntax”// Fire-and-forgetspawn some_function(arg1, arg2)
// Capture handle for awaitlet handle = spawn some_function(arg1, arg2)Arguments
Section titled “Arguments”Arguments to spawn are captured by value. The scheduler heap-allocates a thunk containing the function pointer and all argument values. This is safe because fibers may outlive the spawning stack frame within the nursery scope.
func main() do let name = "Alice" nursery do // 'name' is copied into the spawn thunk -- safe even if // main's stack frame were to change (it won't, nursery blocks, // but the invariant holds for nested spawns) spawn greet(name) endendReturn Value
Section titled “Return Value”spawn returns a task handle — an opaque reference to the spawned fiber’s result slot. The handle is used with await to retrieve the task’s return value.
If you do not need the result, discard the handle:
spawn background_work() // handle discarded -- fire-and-forgetThe task still runs; you simply choose not to collect its result. The nursery still waits for it to complete.
Spawn Limits
Section titled “Spawn Limits”Nurseries enforce a spawn budget (configurable via Budget.spawn_count). Attempting to spawn beyond the budget returns a BudgetExhausted error. Default service budget allows 1024 concurrent spawns per nursery.
await suspends the current fiber until the target task completes, then returns the task’s result.
Syntax
Section titled “Syntax”let result = await handleWith error handling:
let result = await handle catch |err| do // Handle task failure default_valueendSemantics
Section titled “Semantics”| Scenario | Behavior |
|---|---|
| Task already finished | Returns immediately (fast path — atomic CAS check) |
| Task still running | Current fiber yields; scheduler runs other work |
| Task was cancelled | Returns Cancelled error |
| Task failed | Returns the task’s error |
Individual Task Await
Section titled “Individual Task Await”Each await targets a specific task handle. This is an individual await — it does not wait for all nursery children, only the specified one.
The protocol uses an atomic CAS waiter registration:
- Awaiter atomically registers itself on the target task’s waiter slot
- If the task is already done, CAS fails and the awaiter reads the result immediately
- If the task completes while the awaiter is parked, the completing fiber wakes the awaiter
This gives O(1) await with no lock contention.
Concurrent Await
Section titled “Concurrent Await”Multiple fibers can await different tasks simultaneously:
nursery do let h1 = spawn compute_a() let h2 = spawn compute_b() let h3 = spawn compute_c()
// These three awaits run concurrently -- // the fiber yields on each until that specific task completes spawn func() do let a = await h1 use_result_a(a) end
spawn func() do let b = await h2 use_result_b(b) end
let c = await h3 use_result_c(c)endChannel
Section titled “Channel”Typed, bounded, closeable message-passing channels for inter-task communication. Follows the Communicating Sequential Processes (CSP) model.
Creation
Section titled “Creation”let ch = channel(capacity)capacity = 0— Unbuffered (rendezvous).sendblocks until arecvis ready.capacity > 0— Buffered. Up tocapacitymessages can be queued beforesendblocks.
The channel type is inferred from usage:
let ch = channel(10) // Channel(i64) inferred from first send/recvch.send(42)Or declared explicitly:
let ch: Channel(String) = channel(10)ch.send(value)Send a value into the channel. If the buffer is full, the calling fiber yields until space is available. If the channel is closed, send fails with ChannelClosed.
let value = ch.recv()Receive a value from the channel. If the buffer is empty, the calling fiber yields until a value is available. Returns null when the channel is closed and empty — this is the signal that no more values will arrive.
Idiomatic consumption loop:
while let msg = ch.recv() do process(msg)end// Channel closed, all messages consumedtrySend
Section titled “trySend”let ok = ch.trySend(value)Non-blocking send. Returns true if the value was enqueued, false if the buffer was full. Does not yield.
tryRecv
Section titled “tryRecv”let maybe_value = ch.tryRecv()Non-blocking receive. Returns the value if one was available, null otherwise. Does not yield. Does not distinguish between “empty buffer” and “closed channel” — use isClosed() to disambiguate.
ch.close()Close the channel. After closing:
send()fails withChannelClosedrecv()drains remaining buffered values, then returnsnullisClosed()returnstrue
Closing is idempotent. Calling close() on an already-closed channel is a no-op.
isClosed
Section titled “isClosed”if ch.isClosed() do println("Channel is done")endReturns true if the channel has been closed.
Select
Section titled “Select”select multiplexes over multiple channel operations. It blocks until one of the cases is ready, then executes that case’s body. If multiple cases are ready simultaneously, one is chosen non-deterministically.
Syntax
Section titled “Syntax”select do recv channel_a |value| do // Received from channel_a end recv channel_b |value| do // Received from channel_b end send channel_c(expression) do // Sent to channel_c end timeout milliseconds do // No channel ready within timeout end default do // No channel ready right now (non-blocking) endendCase Types
Section titled “Case Types”recv ch |value| do // value is the received messageendWaits for a value on ch. The |value| binding receives the message. If the channel is closed and empty, this case is skipped.
send ch(expression) do // Executed after successful sendendAttempts to send the evaluated expression into ch. If the buffer is full, the case blocks. If another case becomes ready first, this send is not executed.
timeout
Section titled “timeout”timeout 5000 do // Fired if no other case is ready within 5 secondsendThe value is in milliseconds. At most one timeout case per select. Mutually exclusive with default.
default
Section titled “default”default do // Executed immediately if no channel case is readyendMakes the entire select non-blocking. If any channel case is ready, it wins. If none are ready, default fires. Mutually exclusive with timeout.
Select in Loops
Section titled “Select in Loops”The common pattern is select inside a while loop to continuously react to events:
var running = truewhile running do select do recv work_ch |job| do process(job) end recv quit_ch |_| do running = false end timeout 10_000 do println("Idle for 10 seconds") end endendCancelToken
Section titled “CancelToken”Cooperative cancellation primitive. A CancelToken is a thread-safe, atomic flag that tasks check at yield points to determine if they should stop.
Creation
Section titled “Creation”let token = CancelToken.create()Nurseries create cancel tokens automatically. You can also create standalone tokens for manual cancellation control.
if token.isCancelled() do // Clean up and exit returnendTasks should check cancellation at natural yield points — between iterations of a loop, before starting expensive work, after channel operations.
Cancel
Section titled “Cancel”token.cancel()Sets the cancellation flag. All subsequent calls to isCancelled() return true. This is an atomic operation — safe to call from any fiber.
Nursery Integration
Section titled “Nursery Integration”When a nursery’s cancel token fires (either from a child failure or explicit nursery.cancel()), all children see isCancelled() == true at their next yield point.
nursery |n| do spawn func() do while not n.isCancelled() do do_work() end // Nursery was cancelled -- exit cleanly end
spawn func() do // This failure cancels the nursery, triggering // the other task's isCancelled() check fail FatalError endendBudget
Section titled “Budget”Every nursery and task has a resource budget that limits consumption. Budgets enforce resource discipline and prevent runaway tasks from starving the system.
Budget Fields
Section titled “Budget Fields”| Field | Type | Description |
|---|---|---|
ops | u64 | Maximum operation count |
memory | u64 | Maximum memory allocation in bytes |
spawn_count | u64 | Maximum child spawns |
channel_ops | u64 | Maximum channel send/recv operations |
syscalls | u64 | Maximum system call count |
Budget Defaults
Section titled “Budget Defaults”let b = Budget.serviceDefault() // Generous limits for :service profilelet b = Budget.childDefault() // Per-task budget slicelet b = Budget.zero() // No budget (unlimited -- :core profile)Budget Exhaustion
Section titled “Budget Exhaustion”When a task exhausts any budget dimension, it transitions to BudgetExhausted state. A supervisor task can recharge it:
nursery |n| do let handle = spawn worker_task()
// Monitor and recharge if needed spawn func() do while not n.isCancelled() do if handle.isBudgetExhausted() do handle.rechargeBudget(Budget.childDefault()) end yield() end endendFiber-Based Main
Section titled “Fiber-Based Main”Starting with v2026.3.23-service-async, main() itself runs as a fiber on the M:N scheduler. This means:
main()can usenursery,spawn,await,channel, andselectdirectly- No special setup or scheduler initialization required
- The scheduler starts automatically when the program begins
- Worker threads are created based on available CPU cores
func main() do // This is already running on the fiber scheduler nursery do spawn task_a() spawn task_b() end println("Done")endScheduler Architecture
Section titled “Scheduler Architecture”The underlying scheduler is a Capability-Budgeted Cooperative M:N Scheduler (CBC-MN):
- M:N mapping — M fibers multiplexed onto N OS worker threads
- Work-stealing — idle workers steal tasks from busy workers’ deques (Chase-Lev)
- Cooperative yield — fibers yield at channel operations, await, and explicit yield points
- Per-fiber stacks — each fiber has its own stack (default size varies by profile)
- Context switch — architecture-specific assembly (x86_64, aarch64) — fast register swap
For deep scheduler internals, see M:N Scheduler & Fibers.
Specifications
Section titled “Specifications”| Spec | Description |
|---|---|
| SPEC-021 | Capability-Budgeted Cooperative M:N Scheduler |
| SPEC-019 | Cancellation Tokens and Structured Failure Propagation |
| SPEC-022 | Scheduling Capabilities |
| SPEC-017 | Janus Syntax (nursery/spawn/select grammar) |
“Concurrency without structure is just chaos with more threads.”