Concurrency
spawn with capture lowering, Send/Sync checks, and pthread-backed channels.
spawn
Nyra offers two concurrency backends, selected with a suffix on spawn:
| Syntax | Backend | When to use |
|---|---|---|
spawn { } or spawn:task { } | Lightweight tasks (global worker pool, Tokio-style) | Default — many concurrent jobs; queue up to 65k tasks; workers ≈ cpu_count() |
spawn:thread { } | OS thread (pthread / CreateThread) | Blocking work, long-running isolation, or when you need a real 1:1 thread |
Both forms support:
- Statement — fire-and-forget (handle detached at end of statement).
- Expression —
let h = spawn { ... }returnsJoinHandle; callh.join()to wait (consumes handle).
// Lightweight task (default) — queued on runtime pool
spawn {
print("many tasks OK")
}
let h = spawn:task {
print(99)
}
h.join()
// Dedicated OS thread — ~MB stack, true parallel isolation
let t = spawn:thread {
heavy_blocking_work()
}
t.join()// Lightweight task (default) — queued on runtime pool
spawn {
print("many tasks OK")
}
let h = spawn:task {
print(99)
}
h.join()
// Dedicated OS thread — ~MB stack, true parallel isolation
let t = spawn:thread {
heavy_blocking_work()
}
t.join()Task vs thread: tasks are cheap (bytes–KB of bookkeeping, shared worker pool); OS threads cost ~MB of stack each. Use spawn / spawn:task for high fan-out (thousands+ jobs). Use spawn:thread when you need a real thread or blocking syscall isolation. Captures must be Send in both modes.
Lowering: codegen emits a closure entry + spawn_task_capture (tasks) or spawn_capture (threads). Captures are packed into a struct and copied to the heap. Dropped handles without .join() detach (tasks keep running on the pool; threads are pthread_detached).
allow_extended (file directive)
spawn is a Stable Extended feature. Examples and production code that use spawn should start with:
allow_extended
- What: a file-level directive (one line at the top), not a function attribute.
- Why: declares that this unit intentionally uses Extended-tier APIs (
spawn,parallel for,async,defer, …). - Effect: suppresses
warning[W001]for Extended features in that file — helpful when building with stricter CI flags (--deny-extendedgates Core-only crates). - Not required for Core-only programs (no spawn/async/defer). Omit it in minimal tutorials that do not use Extended syntax.
Without allow_extended, spawn may still compile in default builds, but keeping the directive makes intent explicit and keeps CI configurations predictable.
JoinHandle and .join()
Two ways to use spawn:
| Style | Example | Behavior |
|---|---|---|
| Statement | spawn { work() } | Start background work and return immediately. No handle; runtime detaches (fire-and-forget). |
| Expression | let h = spawn { work() } | Returns JoinHandle — an opaque token representing the in-flight task or thread. |
.join() — method on JoinHandle; signature h.join() -> void (no arguments).
- Blocks the calling thread until the spawned body completes.
- Consumes the handle (move semantics) — after
h.join(),hcannot be used again. - Works for both
spawn/spawn:task(task pool) andspawn:thread(OS thread); codegen picksspawn_task_joinorspawn_joinautomatically. - If a
JoinHandlegoes out of scope without.join(), the runtime detaches — same effect as the statement form.
allow_extended
fn main() {
// Wait for task — output order: 99, then 0
let h = spawn {
print(99)
}
h.join()
print(0)
// Fire-and-forget — main does not wait
spawn {
print("background")
}
}allow_extended
fn main() -> void {
// Wait for task — output order: 99, then 0
let h = spawn {
print(99)
}
h.join()
print(0)
// Fire-and-forget — main does not wait
spawn {
print("background")
}
}Full runnable examples: Built-in methods → JoinHandle · .join() example.
parallel for
Use parallel for when iterations are independent. Each variant below follows: name → explanation → example → output. Full runnable programs: methods gallery · examples/builtins/parallel/.
Name: parallel for / parallel:task
Explanation: Default backend is the global task pool (same workers as spawn). Fork-join: the loop body runs on pooled workers; main continues only after all iterations finish. Requires allow_extended.
allow_extended
fn main() {
parallel for i in 0..4 {
print(i)
}
print(999)
}allow_extended
fn main() -> void {
parallel for i in 0..4 {
print(i)
}
print(999)
}Output (iteration lines may appear in any order; 999 is always last):
0
1
3
2
999
Gallery: parallel for · Run: nyra run examples/builtins/parallel/parallel_task.ny
Name: parallel:task(max = N)
Explanation: Explicit task-pool alias with a worker cap. Prefer max = N over deprecated max_threads — avoids confusion with parallel:thread.
allow_extended
fn main() {
parallel:task(max = 4) for i in 0..1000 {
work(i)
}
}allow_extended
fn main() -> void {
parallel:task(max = 4) for i in 0..1000 {
work(i)
}
}Output: Depends on work(i); the loop is fork-join — code after the loop runs only when every iteration completes.
Gallery: parallel:task(max = N) · Run: nyra run examples/builtins/parallel/parallel_task_max.ny
Name: parallel:thread(max = N)
Explanation: OS-thread fork-join per chunk (~MB stack). Use for blocking I/O or when you need real thread isolation. Backend is chosen by :thread, not by the max key.
allow_extended
fn main() {
parallel:thread(max = 4) for i in 0..1000 {
work(i)
}
}allow_extended
fn main() -> void {
parallel:thread(max = 4) for i in 0..1000 {
work(i)
}
}Output (sample with print(i); order non-deterministic):
2
3
0
1
999
Gallery: parallel:thread · Run: nyra run examples/builtins/parallel/parallel_thread.ny
Backend summary
| Syntax | Backend | When | Example |
|---|---|---|---|
parallel for / parallel:task | Task pool (default) | Most CPU-bound parallel loops | gallery |
parallel:thread / backend = thread | OS thread fork-join | Blocking syscalls, isolation | gallery |
Worker options
| Option | Meaning | Example |
|---|---|---|
max = N | At most N workers | parallel(max = 4) for i in 0..n { … } |
threads = N | Exactly N workers | gallery |
cpu = P% | P percent of logical CPUs | parallel(cpu = 80%) for i in 0..n { … } |
mode = … | auto, balanced, max_performance, background | parallel(mode = balanced) for i in 0..n { … } |
Legacy keys max_threads, max_workers, cores still parse with a deprecation note — use max.
Built-in cpu_count() — gallery: cpu_count(). Extended introspection: stdlib/os/cpu.ny (cpu_logical_cores(), AVX, brand).
| Rule | Why |
|---|---|
No break | Iterations run concurrently; there is no ordered early exit |
| No mutation of outer variables | Would be a data race without explicit locks |
Captures must be Send | Same thread-safety rules as spawn |
Range or fixed array / string / vec_str | Iterable length must be known before workers start |
On wasm32-wasi, parallel for runs sequentially (no pthreads).
Race detection
Job: Catch data races in spawn / parallel / async workloads before they ship.
nyra race . # ThreadSanitizer (recommended on host)
nyra race . --native # portable Nyra lock-set runtime
nyra run . --race
nyra test . --race-native
nyra watch . --on run --race
| Detector | Command | Platform notes |
|---|---|---|
| ThreadSanitizer | nyra race / --race | Linux / macOS / Windows host clang with TSan; not for wasm or cross targets |
| Native lock-set | nyra race --native / --race-native | Works wherever the Nyra runtime links; instrument with stdlib/race.ny |
CLI details: Toolchain → nyra race. Async overview: Async.
benchmark { }
Name: benchmark { … }
Explanation: Prints wall time, RSS delta, and CPU% for a block. Requires allow_extended.
Example: methods gallery · nyra run examples/builtins/benchmark/benchmark.ny
allow_extended
fn main() {
benchmark {
run()
}
}allow_extended
fn main() -> void {
benchmark {
run()
}
}Output:
Time: 0.1 ms
Memory: 0.0 B
CPU: 98%
progress for
Name: progress(label = "…") for x in items { … }
Explanation: Sequential loop with built-in progress bar — not parallel. Optional label; cannot combine with parallel for.
allow_extended
progress(label = "parser tests") for item in tests {
run(item)
}allow_extended
progress(label = "parser tests") for item in tests {
run(item)
}Output (each iteration):
[#####-------] 43%
Running parser tests...
Full example with body output: progress for gallery.
Captures & ownership
fn main() {
let n = 42
spawn {
print(n) // n captured by value (Copy)
}
}fn main() -> void {
let n: i32 = 42
spawn {
print(n) // n captured by value (Copy)
}
}| Capture kind | Compiler rule |
|---|---|
Copy (i32, bool) | Copied into capture struct |
Move (string, structs) | Must be Send; marked moved in parent scope |
Active & / &mut | Compile error |
Full Send/Sync rules: Memory → Send / Sync.
Runtime API
| Symbol | Role |
|---|---|
spawn_capture(body, data, nbytes) | spawn:thread — create joinable OS thread; returns JoinHandle |
spawn_join(handle) | Block until OS thread completes |
spawn_handle_drop(handle) | Detach OS thread if handle dropped without .join() |
spawn_task_capture(body, data, nbytes) | spawn / spawn:task — enqueue on global task pool; returns JoinHandle |
spawn_task_join(handle) | Block until pooled task completes |
spawn_task_handle_drop(handle) | Fire-and-forget task (no wait) |
parallel_for_range(...) | Fork-join parallel loop |
progress_update(cur, total, label) | Draw progress bar + status line |
progress_finish() | Final newline after progress for |
spawn() | Legacy no-arg stub (empty spawn) |
Provided by the Nyra runtime. Not available on wasm32-wasi (stub only).
Channels
Typed channel wrappers live in stdlib/sync/channel.ny and are safe to share across threads.
Stdlib wrappers: stdlib → Sync, context & async channels (Channel_i32, Mutex, Context_with_timeout).
| Runtime symbol | Role |
|---|---|
channel_new() | Create channel; returns handle |
channel_send(ch, value) | Send i32 |
channel_recv(ch) | Blocking receive |
channel_free(ch) | Release channel memory |
Declare extern in your module or see Standard library → Channels.
Spec 1.0 scope
| In 1.0 | Deferred |
|---|---|
spawn { } + C channels | async / await |
parallel for (fork-join pool) | Full goroutine scheduler RFC |
| Blocking I/O via runtime | Full goroutine scheduler RFC |
spawn captures: values must be Send, shared references need Sync inner types, and active borrows are rejected. See Memory → Send / Sync.