Docs/Concurrency

Coroutines

Run waiting work side by side and await the results.

On this page

A coroutine is a function call that runs alongside others on the same thread. While one waits for a timer, a socket or a file, the others keep going.

Start and await#

co starts a call as a coroutine and await waits for its result:

Valk
use valk.time

fn slow_double(value: int) int {
    time.sleep_ms(20)
    return value * 2
}

fn main() {
    let first = co slow_double(10)
    let second = co slow_double(20)
    println((await first) + (await second)) // 60, after about 20 ms
}

Both calls wait at the same time, so the program takes about as long as one of them. Functions that wait, like time.sleep_ms, network calls and file reads, let other coroutines run in the meantime.

The coroutine type#

co returns a value of type co(T), which can be stored, passed around and awaited later. Several results and an error type are written as in a function signature:

Valk
fn min_max(a: int, b: int) (int, int) {
    return a < b ? (a, b) : (b, a)
}

fn start() co(int, int) {
    return co min_max(7, 3)
}

fn main() {
    let task = start()
    let low, high = await task
    println("%low %high") // 3 7
}

Errors#

An error thrown in a coroutine is handled at the await:

Valk
fn parse_count(text: String) int !SyntaxError {
    return text.to(int) !>
}

fn main() {
    let good = co parse_count("42")
    let bad = co parse_count("many")
    println(await good !? 0) // 42
    println(await bad !? 0)  // 0
}
  • A coroutine that is never awaited drops its error silently.
  • Errors thrown by a coroutine cannot carry payload fields; handle those inside the coroutine.

Run a closure#

co also starts a closure, which can capture local values:

Valk
use valk.time

fn main() {
    let name = "Ada"
    let task = co fn() String {
        time.sleep_ms(5)
        return "hello " + name
    }()
    println(await task) // hello Ada
}

Many coroutines#

Keep the coroutines in an array and await them in turn:

Valk
use valk.time

fn work(id: int) int {
    time.sleep_ms(10)
    return id
}

fn main() {
    let tasks = Array[co(int)]{}
    each 0..10 as id : tasks.append(co work(id))
    let total = 0
    each tasks as task : total += await task
    println(total) // 45
}

To wait for whichever finishes first, send results over a channel.

Things to know#

  • Coroutines share one thread. CPU-heavy work does not get faster; use threads for that.
  • coro.await_last() from valk.coro lets the other coroutines run before continuing.
  • core.Mutex and Lock[T] waits only pause the current coroutine. See Sharing data between threads.
  • Each coroutine has a 1 MB stack; very deep recursion ends with a Stack overflow panic. Keep large buffers in an Array instead of a fixed array.