Skip to main content

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, each exec/submit from the host, and each network/timer callback runs as a fiber.
  • Fibers share one recursive mutex for engine state — all insert/at/exec access 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 pointWhat happens
Loop bodies — every 64 statementsA 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

CategoryPages
CoreThis overview + Spawn & Await is 80% of what you need
PrimitivesSleep, Defer, Race
PracticePatterns — fan-out, many futures, parallel HTTP
FailureErrors — 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.