- README: shipped concurrency/HTTP/WebSockets sat in the roadmap as "not yet available"; "no package manager" contradicted [deps]; the deps example would not have compiled (the key IS the module name) - runtime/README: leads with wovm, wo-rt.c demoted to a historical section; dropped 2 nonexistent recipes, crates/rt, @gc refcounting, 13 suites -> 18 - employee + log-watcher READMEs claimed "does not compile"; both are gates - error catalog: +10 emitted codes incl WO-E250, the only diagnostic the shipped query surface raises; recorded why the sweep rotted - language-surface: group-by parses, then the typechecker refuses it - 00-code-review + 00-link-audit re-run; history kept, not rewritten - 48 dead Rust-era exploration links de-linked rather than re-pointed (their prose names the retired plan by number); successor map -> discarded.md - 08-project-structure: compiler/plan/ never existed; corpus has 9 dirs, 5 empty - releasing.md: dropped a --draft step the workflow never had - new docs/00-doc-audit.md: findings + disposition, incl one row where the audit was wrong and the doc it accused was right - status folders removed: 34 stories flat, status only in frontmatter; 252 links recomputed from resolved paths; board/board-views/structure retaught - story 24 -> in-progress, since frontmatter is now the only truth - new iteration 38: fs mutation verbs + net.connect, the two capability families no iteration owned - new iteration 39: gofiber/fiber v3.5.0 parity study. The ledger called CSRF/sessions unblocked by iteration 34's HMAC, but the runtime has no source of randomness at all - linkcheck skips .dev/.superpowers: 0 broken paths, 0 bad anchors Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
159 lines
8.3 KiB
Markdown
159 lines
8.3 KiB
Markdown
# Fibers — the concurrency model, its terminology, and why writeonce chose what it chose
|
||
|
||
> Exploration/reference note (no status banner by board convention).
|
||
> The normative decisions live in the arc spec
|
||
> ([`2026-08-20-shard-fiber-arc-design.md`](../../../superpowers/specs/2026-08-20-shard-fiber-arc-design.md))
|
||
> and iterations [8](../../../stories/language-runtime-database/08-shard-actor-runtime.md) /
|
||
> [11](../../../stories/language-runtime-database/11-fibers.md); this
|
||
> page explains the WHY at doctrine depth. Written 2026-08-20, when this
|
||
> file was also the target of a dangling reference from iteration 11 —
|
||
> it exists now.
|
||
|
||
> **Demonstrated live** by [`docs/examples/fibers`](../../../examples/fibers/README.md)
|
||
> (`just fibers`, 8 checks): byte-exact budget interleave, a parked
|
||
> sleeper blocking nobody, on the io_uring AND epoll backends.
|
||
|
||
## What a green thread is, and why anyone bothers
|
||
|
||
A green thread (fiber, virtual thread, coroutine — the tradition picks
|
||
the name) is a thread scheduled by the LANGUAGE RUNTIME, not the kernel.
|
||
The kernel sees a handful of OS threads; the runtime multiplexes
|
||
thousands-to-millions of logical threads on top.
|
||
|
||
The economics: an OS thread costs a fixed stack reservation (typically
|
||
1–8 MiB of address space) and every context switch goes through the
|
||
kernel scheduler (CFS is O(log N), and task counts in the hundreds
|
||
already hurt). A green thread can start around 2 KiB and switch in tens
|
||
of nanoseconds — a register save and a pointer swap, no syscall. That is
|
||
the difference between a few thousand concurrent connections and a few
|
||
million.
|
||
|
||
## The terminology, untangled
|
||
|
||
- **Green thread** — originally Java pre-1.3's model, which was M:1
|
||
(everything on ONE OS thread — no parallelism at all). Now used
|
||
loosely for any user-space thread.
|
||
- **Fiber** — usually implies COOPERATIVE: it runs until it explicitly
|
||
yields.
|
||
- **Coroutine** — the broadest term; covers stackful and stackless both.
|
||
- **Goroutine** — Go's M:N implementation, preemptively scheduled.
|
||
- **Virtual thread** — Java 21's, also M:N.
|
||
|
||
The load-bearing distinction between "fiber" and "goroutine" is NOT the
|
||
stack — it is the EVICTION POLICY. A classic fiber owns the CPU until it
|
||
yields; one selfish loop starves everything behind it. A goroutine can
|
||
be interrupted WITHOUT ITS CONSENT (since Go 1.14, via a signal that
|
||
stops it at a safe instruction).
|
||
|
||
M:1 vs M:N matters concretely here: one process embedding its own
|
||
storage would leave every core but one idle under M:1. writeonce is M:N
|
||
— stage 2 of the arc pins one kernel thread per core (shards), and
|
||
fibers multiplex above them.
|
||
|
||
## writeonce's model: cooperative mechanics, accounting-based eviction
|
||
|
||
The BEAM shape. Mechanically cooperative — no signals, no interrupts,
|
||
no async preemption machinery — but every loop back-edge decrements a
|
||
REDUCTION BUDGET (`WO_REDUCTIONS`, default 4000), and at zero the fiber
|
||
is re-queued whether it likes it or not.
|
||
|
||
What that buys, all three at once:
|
||
|
||
- goroutine-grade starvation-freedom (the corpus pins budget-1 as EXACT
|
||
round-robin across fibers — `runtime/test/test_fiber.c`);
|
||
- fiber-grade DETERMINISM: same program, same schedule, byte-identical
|
||
output — signal-based preemption points are nondeterministic by
|
||
nature, Go's included;
|
||
- zero interruption machinery: an interpreter's dispatch loop is always
|
||
at a safe point, so the "where can I safely stop this thread"
|
||
problem Go solves with signal handlers and pc maps costs nothing.
|
||
|
||
One implementation lesson worth keeping (it cost a livelock): the
|
||
budget check runs at loop back-edges AFTER the jump lands, so the saved
|
||
resume pc is the loop head. A pre-instruction save at budget 1
|
||
re-executes the jump straight into the same decrement, forever.
|
||
|
||
## Should the grammar have `async`? No — permanently rejected
|
||
|
||
`async` is only NECESSARY in stackless designs, where the compiler must
|
||
know statically which functions can suspend so it can transform each
|
||
into a state-machine struct. That requirement is function COLORING: an
|
||
async function can only be called from another async function, so the
|
||
keyword propagates virally up every call chain, and the ecosystem forks
|
||
into sync and async flavors of everything. In writeonce it would color
|
||
the query API, then every handler that touches data — the exact code
|
||
that most wants to read as straight-line logic.
|
||
|
||
With stackful fibers, a blocking read ten frames deep just parks. The
|
||
suspension point is a RUNTIME fact, not a TYPE-SYSTEM fact. Same source
|
||
text in program mode and server mode; no keyword; no coloring. This is
|
||
iteration 11's "async/await — permanently rejected surface, not
|
||
deferred", with the reasoning attached.
|
||
|
||
## Stackful vs stackless: the real fork, and the recommendation
|
||
|
||
**Stackful** (goroutines, Java virtual threads, classic fibers): each
|
||
logical thread owns a real, growable stack; it can suspend from ANY
|
||
depth. Costs: growth-by-copy must find and rewrite every pointer into
|
||
the old stack (Go's machinery), and FFI is the classic killer — C
|
||
callees assume a large contiguous stack and know nothing about growth,
|
||
so every foreign call needs a stack switch or a guaranteed segment.
|
||
|
||
**Stackless** (Rust futures, JS promises, C# async, Python coroutines):
|
||
the compiler rewrites each async function into a fixed-size state
|
||
object. Cheaper, runtime-optional — and it buys the coloring above,
|
||
plus suspension only at explicit await points.
|
||
|
||
**Recommendation, and what is already built: STACKFUL — because both of
|
||
its classic costs vanish in this stack.**
|
||
|
||
1. **No FFI, by doctrine.** The main argument against small growable
|
||
stacks is C interop. writeonce has no C to call (the reject row is
|
||
load-bearing). The cost is simply absent.
|
||
2. **The "stack" is register-indexed, not address-based.** A
|
||
`wo_fiber`'s state is the interpreter's register window + frame
|
||
array — frames reference registers by INDEX, so a context is
|
||
relocatable by construction. Growth is allocate-larger + memcpy with
|
||
ZERO pointer fixups; Go's hardest stackful problem does not exist
|
||
here. Contexts are ~42 KiB fixed today; start-small (~4 KiB)
|
||
growable contexts are the recorded improvement that takes fiber
|
||
counts from thousands toward millions (arc spec, "Fiber context
|
||
growth").
|
||
|
||
## The two problems every design must solve, and where they land here
|
||
|
||
- **Blocking syscalls.** A green thread calling fsync blocks the OS
|
||
thread carrying it and strands every fiber behind it — and an
|
||
embedded database fsyncs on every commit, milliseconds each, an
|
||
eternity for a scheduler. writeonce's answer is io_uring, twice: the
|
||
arc's per-shard ring parks blocking `net`/`time` builtins (T4,
|
||
readiness ops first, then reads/writes as ring ops), and iteration
|
||
23 rides the SAME ring for WAL WRITE+FSYNC group-commit so the
|
||
DB-owner shard keeps executing while durability drains. epoll
|
||
survives only as the portability fallback behind a startup probe
|
||
(`WO_IO=uring|epoll`) — seccomp'd containers routinely deny io_uring,
|
||
and the binary must run everywhere. The reference cards live in
|
||
[`../linux/07-io_uring.md`](../linux/07-io_uring.md).
|
||
- **Preemption.** Solved by the reduction budget above. And to the
|
||
standing question "does preemption depend on FFI?" — NO, inversely:
|
||
preemption holes come from frames the runtime cannot interrupt, which
|
||
in Go means foreign C frames (its signal preemption skips them). No
|
||
FFI means no foreign frames; the only non-preemptible regions are our
|
||
OWN builtins — C we control, bounded, and the blocking ones become
|
||
parked ring ops. FFI would have CREATED the dependency; its absence
|
||
is why the budget is airtight.
|
||
|
||
## Precedent survey (what was adopted, what was rejected)
|
||
|
||
- **BEAM (Erlang/Elixir): adopted** — reduction-budget preemption,
|
||
actor mailboxes, one-message-at-a-time delivery, isolated failure
|
||
(a fiber's uncaught trap kills that fiber alone).
|
||
- **Go: half-adopted** — M:N over pinned threads yes; stack-copy
|
||
pointer rewriting unnecessary here (register-indexed contexts);
|
||
signal-based async preemption rejected (nondeterministic, and the
|
||
budget makes it redundant).
|
||
- **Rust/Tokio, JS, C#, Python: rejected** — stackless coloring is the
|
||
cost this language exists to not pay.
|
||
- **Java Loom: matches** — park-under-a-blocking-API is exactly the
|
||
stdlib posture (`net.read` blocks in program mode, parks under
|
||
fibers, same signature).
|