Skip to content
Kavachकवच

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
syntax: Kavach code
WrittenHow the task runsBest for
spawnA 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 threadAn 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 coresOn a worker thread per core, the next one in turn, started when the first such task is spawnedHeavy computing, spread over every core, and a server's task per connection (http.serve uses it, 14.11)
spawn processAn operating-system process of its own, running the program's code; the call's arguments and its result are copied in and outCode 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
spawn-kinds.kav: Kavach code, then what running it prints
  • These three take the same things, a call or a block, and each gives a Task<T>.
  • thread, cores and process are words only right after spawn; 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 cores task 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) runs spawn cores tasks on every core today. kavach run runs 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:

CaseNo lookLook that gives upC++, blocking sockets
spawn cores, 1 client, 20,000 trips2,217–2,464279–3242,129–2,184
spawn cores, 100 clients, 1,000 trips each1,027–1,175445–579298–383
Plain spawn server, 1 client in another program, 20,000 trips2,064–2,343621–7021,992–2,185
Plain spawn server, 100 clients in another program, 1,000 trips each3,717–3,7213,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 spawn server 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. Plain spawn stays on one core, by design, so C++ on all cores is far ahead there.
  • A slow plain spawn server'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 cores program 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. Plain spawn and 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
spawn-process.kav: Kavach code, then what running it prints
  • 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() or within ends 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() gives panicked: <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, which spawn process sets. 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)
spawn-process-block.kav: Kavach code, then what running it prints

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)
spawn-process-shared.kav: Kavach code, then what running it prints

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)
kavach.toml: TOML code, then what running it prints