Skip to content
You are reading next, which describes the unreleased compiler. Khora is unstable before v1: syntax, standard-library APIs and behavior may change. The current release is documented under v0.4.

Fibers

A fiber is a Khora computation that can suspend — waiting on a socket, a timer, a channel — and continue later, while other fibers run.

Khora ships two implementations of the same interface, and picks one at run time.

Threads, by default. Each fiber is an operating-system thread. Suspending is the thread blocking; waking is the operating system scheduling it. The kernel does the work, the stacks are the kernel’s, and a blocking foreign call blocks only the fiber that made it.

A scheduler, with KHORA_FIBERS=scheduler. Each fiber is a stackful coroutine on a pool of worker threads, with work stealing. Suspending is a stack switch in user space, and a fiber can move between workers.

They behave identically except under cancellation: a fiber inside clock.sleep is woken by a cancellation under the scheduler, but runs the sleep to completion under threads. Known limitations has the measurements.

Threads are the default because they are faster at the connection counts a service actually runs at; the scheduler exists for programs that need far more concurrent fibers than a machine has threads. Known limitations has the measurements.

The remaining difference is cost and density:

thread coroutine
stack 8 MB, reserved and committed as it is touched 8 MB with a guard page, the same
suspend a kernel transition a stack switch
how many thousands hundreds of thousands

Each worker has its own queue and takes from the front. A worker with nothing to do steals half of another worker’s queue from the back, so the owner and the thief contend for the lock but never for the same task. Every so often a worker checks a shared queue first, so a fiber woken from outside the pool — by the reactor, or by another thread — is not left waiting behind a worker’s own work.

A fiber woken this way can resume on a different worker than it started on. Nothing a fiber owns is tied to a thread: its identity, its cancellation state and its current span travel with it. The one exception is foreign code that is itself thread-affine, which FFI covers.

What makes Khora’s concurrency structured is that a fiber cannot outlive the block that started it. That is not a separate mechanism — it is regions, which already run their finalizers on every way out of a block.

A nursery opens a region and installs a Nursery capability whose spawn registers each fiber with it. Every path out of the block runs the finalizers, and those wait for the children: running off the end, an early return, a raise passing through, a cancellation.

There is nothing extra to enforce, and no way to write a fiber that escapes: the Nursery capability is in scope only inside the block, and the block cannot end while a child is still running.

nursery(fn () => {
nursery.spawn(fn () => serve(first));
nursery.spawn(fn () => serve(second));
}) // both have finished by here, whichever way the block ended

A nursery waits for its children on the way out, and cancels them first if it is leaving because something failed. The distinction is not something the program asks about: cancellation is idempotent, so “cancel then wait” is correct on both paths and the normal one simply has nothing to cancel.

Cancellation is cooperative, at cancellation points

Section titled “Cancellation is cooperative, at cancellation points”

Canceling a fiber does not stop it where it stands. It sets a flag, and the fiber notices at its next cancellation point.

They are:

  • a ! site, which is also where propagation and suspension are marked;
  • a loop back-edge — the point where loop or while goes round again;
  • a call to a function that can itself reach a cancellation point, and the entry of a function that recurses; and
  • a blocking operation: channel sends and receives, waiting on or joining a fiber, clock.sleep, and socket accepts, reads and writes.

Every function has them, whatever its raises row. A function that can reach a cancellation point returns a small tag beside its answer, whether or not it can fail, and a canceled fiber unwinds on that tag the same way a raise unwinds, running each frame’s releases and each region’s finalizers as it goes. A function that reaches none — straight-line arithmetic, a field read — is compiled without the tag, because there is nothing in it to stop at.

This is why a canceled program closes its files. The back-edge is why the ordinary shape of a periodic job can be stopped at all:

fn ticker() with { clock: Clock } {
loop { clock.sleep(200); }
}

There is no ! and no raises row in that body. Without the back-edge, a nursery that had to unwind past it would wait for ever.

A cancellation is not an error: no catch sees one, _ arm included. It passes through and carries on unwinding.

A handler runs and returns; it cannot capture the rest of the computation. All suspension belongs to fibers, which is what lets handlers be a function call rather than a stack switch — see Effects and handlers.

The practical consequence is that with blocks and nurseries compose without either knowing about the other, and a capability handed to a fiber is just a value it captured.

Khora 0.4.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource