Concurrency

spawn with capture lowering, Send/Sync checks, and pthread-backed channels.

spawn

Nyra offers two concurrency backends, selected with a suffix on spawn:

SyntaxBackendWhen 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:

// 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

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:

StyleExampleBehavior
Statementspawn { work() }Start background work and return immediately. No handle; runtime detaches (fire-and-forget).
Expressionlet 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).

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")
    }
}

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)
}

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)
    }
}

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)
    }
}

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

SyntaxBackendWhenExample
parallel for / parallel:taskTask pool (default)Most CPU-bound parallel loopsgallery
parallel:thread / backend = threadOS thread fork-joinBlocking syscalls, isolationgallery

Worker options

OptionMeaningExample
max = NAt most N workersparallel(max = 4) for i in 0..n { … }
threads = NExactly N workersgallery
cpu = P%P percent of logical CPUsparallel(cpu = 80%) for i in 0..n { … }
mode = …auto, balanced, max_performance, backgroundparallel(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).

RuleWhy
No breakIterations run concurrently; there is no ordered early exit
No mutation of outer variablesWould be a data race without explicit locks
Captures must be SendSame thread-safety rules as spawn
Range or fixed array / string / vec_strIterable 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
DetectorCommandPlatform notes
ThreadSanitizernyra race / --raceLinux / macOS / Windows host clang with TSan; not for wasm or cross targets
Native lock-setnyra race --native / --race-nativeWorks 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()
    }
}

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)
}

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)
    }
}
Capture kindCompiler rule
Copy (i32, bool)Copied into capture struct
Move (string, structs)Must be Send; marked moved in parent scope
Active & / &mutCompile error

Full Send/Sync rules: Memory → Send / Sync.

Runtime API

SymbolRole
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 symbolRole
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.0Deferred
spawn { } + C channelsasync / await
parallel for (fork-join pool)Full goroutine scheduler RFC
Blocking I/O via runtimeFull goroutine scheduler RFC
Send / Sync The compiler checks spawn captures: values must be Send, shared references need Sync inner types, and active borrows are rejected. See Memory → Send / Sync.