Concurrency — Overview
Scrii concurrency is cooperative: scripts run as fibers (C++20 coroutines with small heap frames, not OS stacks) over a fixed pool of worker threads. A fiber never preempts another — it only suspends at explicit yield points, and the scheduler resumes the next ready fiber.
This makes thousands of fibers cheap and eliminates data races from preemption, but it also means a fiber that never yields can starve others until its next yield point.
The fiber model
- Each
spawn/defer/race, eachexec/submitfrom the host, and each network/timer callback runs as a fiber. - Fibers share one recursive mutex for engine state — all
insert/at/execaccess is mutex-guarded, so host threads can call the engine while scripts run. But script-level shared variables (globals mutated by multiple fibers) are not auto-synchronized — coordinate via futures or treat counters as approximate. - Callbacks (timers,
on_receive,on_message) reach data through globals, not lexical capture — functions see only globals (see Functions — Globals, not lexical closures).
Yield points — when a fiber suspends
A fiber only yields voluntarily:
| Yield point | What happens |
|---|---|
| Loop bodies — every 64 statements | A while/for/foreach body with one statement yields every 64 iterations. A tight while (true) { n++ } therefore does not lock the engine forever — it yields every 64 increments. |
std.async.sleep(ms) | Yields, polls the clock, resumes after ms. See Sleep. |
std.async.await(future) | Yields until the future completes. See Spawn & Await. |
While one fiber waits at sleep/await, other ready fibers run on the same pool, so a sleeping script never blocks the engine.
:::tip Keep the script alive
Network listeners, timers, and deferred work all need the main script to stay alive. A script that spawns and then exits abandons its background fibers — use sleep or await to keep it running (see Patterns).
:::
API surface
| Category | Pages |
|---|---|
| Core | This overview + Spawn & Await is 80% of what you need |
| Primitives | Sleep, Defer, Race |
| Practice | Patterns — fan-out, many futures, parallel HTTP |
| Failure | Errors — awaiting failures vs last_error() |
Timers (std.time.timer) and std.net servers use the same fiber machinery — a repeating timer or http_server handler is just a background fiber whose errors must be read via last_error() (see Errors).
Quick example
// 3 tasks that each sleep 300 ms — run concurrently in ~300 ms, not 900 ms
var job = fn(name) { std.async.sleep(300); return name }
var t0 = std.time.now()
var f1 = std.async.spawn(job, "a")
var f2 = std.async.spawn(job, "b")
var f3 = std.async.spawn(job, "c")
var results = [std.async.await(f1), std.async.await(f2), std.async.await(f3)]
print("concurrent:", std.time.elapsed(t0), "ms", results) // e.g. 301 ms ["a","b","c"]
Next: Spawn & Await for the core primitive, or Patterns to see common combinations in practice.