How Tasks Run
Also known as: spawn thread spawn cores spawn process coroutine green thread goroutine os thread parallel multicore worker pool async runtime subprocess child process fork multiprocessing isolate sandbox hang
● Advanced6 minBefore this: Taking TurnsSpec 9.8
Each spawn says how its task runs: plain spawn is a coroutine, spawn thread gets a thread of its own, spawn cores runs on any core, spawn process gets a process of its own. The turns, and the output, stay the same.
spawn handle(conn) // a coroutine: the default
spawn thread read_sensor() // a thread of its own
spawn cores render(frame) // on any core of the machine| Written | How the task runs | Best for |
|---|---|---|
spawn | A coroutine on the thread of the task that spawned it: passing the turn needs no operating system (decision 0048) | Many tasks that mostly wait on each other, such as the stages of a pipeline passing values on |
spawn thread | An operating-system thread of its own; a release build (kavach build) lets it compute ahead on another core until its first effect (9.7) | A task that blocks inside a C library, or computes for long |
spawn cores | On a worker thread per core, the next one in turn, started when the first such task is spawned | Heavy computing, spread over every core, and a server's task per connection (http.serve uses it, 14.11) |
spawn process | An operating-system process of its own, running the program's code; the call's arguments and its result are copied in and out | Code Kavach can't check, such as a C or Python library that may hang: cancel() ends the process at once |
Three of them in one program: a producer on the cores, a slow task on a thread of its own, a fast coroutine:
use std.time
fn report(name: String, ms: Int) -> String {
time.sleep_ms(ms)
print("{name} done")
name
}
fn main() ! Error {
const (send, receive) = Channel<Int>(capacity: 4)
const _producer = spawn cores {
for i in 0..10 {
send.put(i)
}
send.close()
}
let total = 0
for n in receive {
total += n
}
const slow = spawn thread report("slow", 60)
const fast = spawn report("fast", 20)
print("both started, total {total}")
print("{try slow.wait()} and {try fast.wait()} are back")
}The program ran and printed:
kavach run spawn-kinds.kav
both started, total 45fast doneslow doneslow and fast are back
- These three take the same things, a call or a block, and each gives a
Task<T>. thread,coresandprocessare words only right afterspawn; anywhere else they are names like any other.- Whichever you choose, tasks take turns in the same order, so the program prints the same: only its speed changes. A
spawn corestask keeps a looser order: its own lines come out in order, whole, and a line it prints before it sends a value, spawns a task or ends comes before what the receiver prints after that. - Only a release build (
kavach build) runsspawn corestasks on every core today.kavach runruns them on one core, in turn, and prints the same.
Waiting for the network
A thread with nothing to run looks at its sockets for a moment before it sleeps. A reply usually comes in that moment, so the thread skips going to sleep and being woken. Tasks of every kind do this, and there is nothing to write in your code. When the looks keep finding nothing, because replies come later than a look lasts, the thread gives up: it sleeps at once, and only looks again now and then. Measured on Linux in WSL, in ms, as ranges over several rounds:
| Case | No look | Look that gives up | C++, blocking sockets |
|---|---|---|---|
spawn cores, 1 client, 20,000 trips | 2,217–2,464 | 279–324 | 2,129–2,184 |
spawn cores, 100 clients, 1,000 trips each | 1,027–1,175 | 445–579 | 298–383 |
Plain spawn server, 1 client in another program, 20,000 trips | 2,064–2,343 | 621–702 | 1,992–2,185 |
Plain spawn server, 100 clients in another program, 1,000 trips each | 3,717–3,721 | 3,740–3,850 |
- One client beats C++ here only because C++'s thread sleeps in each
recv. A C++ program that polled would gain the same. - With 100 clients on
spawn cores, C++ is still ahead. - A plain
spawnserver with 100 busy clients gains nothing: its one worker never has nothing to do. C++ wasn't run that day, so its cell is empty. Plainspawnstays on one core, by design, so C++ on all cores is far ahead there. - A slow plain
spawnserver's CPU doesn't change: with one client 200 µs apart, 0.36–0.44 s without a look and 0.40–0.42 s with it. - A mostly idle
spawn coresprogram does pay: 197% → 274% of a core, with 100 clients waiting 5 ms between trips. - On Windows the look on
spawn cores, measured before it learned to give up, moved neither the time nor the CPU. Plainspawnand giving up were not measured there on their own. What takes the time on Windows is something else, not yet profiled.
A process of its own
spawn process f(args) starts the program again, and the copy runs only that call. It is for code Kavach can't check, such as a C or Python library that may hang: a process can be ended at once.
type Page {
title: String
words: Int
}
fn count(pages: [Page]) -> Int {
print("counting {pages.len()} pages")
let total = 0
for page in pages {
total += page.words
}
total
}
// Stands in for a library call that never returns.
fn convert(file: String) -> String {
let x = file.len()
while true {
x = x % 1000 + 1
}
file
}
fn main() ! Error {
const pages = [Page(title: "Home", words: 120), Page(title: "About", words: 80)]
const job = spawn process count(pages)
print("started")
print("total {try job.wait()} words")
const stuck = spawn process convert("report.doc") within 200.ms
const _pdf = stuck.wait() catch e => {
print(e)
""
}
}The program ran and printed:
kavach run spawn-process.kav
startedcounting 2 pagestotal 200 wordstimed out after 200 ms
- The arguments go in as copies, and the value or the error comes back through
.wait(), as from any task. What the process prints comes out in turn, like any task's lines. cancel()orwithinends the process at once. It shares nothing, so nothing is left half-changed. A thread is never ended this way: one stopped in the middle could leave what it shares half-changed.- A panic in the call fails its task, as in any task:
.wait()givespanicked: <message> at <file>:<line>, and the program goes on. - Starting a process takes milliseconds, so it is for long or risky work.
- The copy knows its call from
KAVACH_PROCESS_CALL, whichspawn processsets. It is not for setting by hand.
It takes a call of a function, not a block:
fn main() ! Error {
const job = spawn process {
print("in a block")
1
}
print("{try job.wait()}")
}The compiler refused the program:
kavach run spawn-process-block.kav
error: `spawn process` takes a call of a function, not a block: put the block in a function and call it (spec 9.8)The arguments and the result go between the processes as JSON, so each must have a JSON form. A Shared value, a channel, a task, a function or an agent has none, and the compiler says which one:
fn add(total: Shared<Int>, n: Int) {
total.update(t => t + n)
}
fn main() ! Error {
const total = Shared(0)
const job = spawn process add(total, 5)
try job.wait()
}The compiler refused the program:
kavach run spawn-process-shared.kav
error: `Shared<Int>` can't be written as JSON, so it can't go to another process (spec 9.8)Refused for now: a generic function, and Bytes going in or out, since Bytes has no JSON form yet. In the interpreter, spawn process works under kavach run; a tool that runs the interpreter inside itself stops with a panic that says to use kavach run.
How tasks run is chosen in the code, not in kavach.toml. The old tasks = setting says where it went:
[package]
name = "server"
tasks = "threads"The compiler refused the program:
error: `tasks` in `[package]` moved into the code: write `spawn thread …` or `spawn cores …` where a task should run another way; a plain `spawn` is a coroutine (spec 9.8, decision 0069)