Concurrency
Kodo uses a cooperative scheduler to run spawned tasks. You express concurrency with spawn blocks, and the runtime executes them after main returns.
The Cooperative Scheduler
Kodo’s concurrency model is based on a task queue. When you use spawn, the block is not executed immediately. Instead, it is enqueued as a task. After main finishes, the runtime calls kodo_run_scheduler, which drains the queue and runs each task in order.
Tasks may spawn additional tasks. The scheduler loops until the queue is empty, so nested spawns are fully supported.
Basic Spawn
A spawn block runs a piece of code as a deferred task:
fn main() -> Int {
spawn {
println("hello from a task")
}
println("main finishing")
return 0
}
Output:
main finishing
hello from a task
The spawned task runs after main returns, which is why “main finishing” appears first.
Spawn with Captures
Spawned blocks can reference variables from the enclosing scope. The compiler performs capture analysis and packs the captured values into an environment buffer that is passed to the task function at execution time.
fn main() -> Int {
let greeting: Int = 42
spawn {
print_int(greeting)
}
let a: Int = 10
let b: Int = 32
spawn {
print_int(a + b)
}
println("all tasks queued")
return 0
}
Output:
all tasks queued
42
42
How Captures Work
Internally, the compiler transforms a spawn block into a top-level function via lambda lifting. Captured variables are serialized into an environment buffer (env packing):
- The compiler identifies free variables in the spawn body.
- Each captured value is written to a contiguous byte buffer at a known offset.
- The task function receives a pointer to this buffer and reads the values back.
This means captures are by value — the task receives a copy of each variable at the time of the spawn, not a reference to the original.
Multiple Tasks
You can spawn as many tasks as you need. They execute in the order they were enqueued:
fn main() -> Int {
spawn {
println("[Task 1] no captures")
}
let x: Int = 10
spawn {
print_int(x)
}
let a: Int = 10
let b: Int = 32
spawn {
print_int(a + b)
}
println("[main] all tasks queued, finishing main")
return 0
}
Complete Example
module async_tasks {
meta {
purpose: "Demonstrate spawn with captured variables",
version: "1.0.0",
author: "Kodo Team"
}
fn main() -> Int {
spawn {
println("[Task 1] no captures")
}
let greeting: Int = 42
spawn {
print_int(greeting)
}
let a: Int = 10
let b: Int = 32
spawn {
print_int(a + b)
}
println("[main] all tasks queued, finishing main")
return 0
}
}
Compile and run:
cargo run -p kodoc -- build async_tasks.ko -o async_tasks
./async_tasks
Channels
Kōdo supports channels for communication between spawned tasks. A channel provides a unidirectional message queue that producers can send values into and consumers can receive from.
:::caution[Channel Limitations]
Channels currently support only Int, Bool, and String value types. Generic channels (Channel<T> for arbitrary T) are not yet available.
:::
Async Syntax Preview
Kōdo supports async fn and .await as syntax, but these currently execute sequentially — true concurrency is planned for a future release. The syntax exists to establish conventions so that code written today will work with real async I/O when it becomes available. For now, use spawn for deferred task execution.
Next Steps
- Actors — stateful actors with message passing and the scheduler
- HTTP & JSON — making HTTP requests and parsing JSON
- Closures — closures, lambda lifting, and higher-order functions