Download

16. Concurrency

A program runs on one thread, with cooperative concurrency through await. I/O in std/io blocks. A child process started with spawn_async (std/process) runs while the program awaits it.

async/await#

A function that uses await is an async function.

  • It declares -> Async<T> (Async<void> for no value), and its body returns a T.
  • A call to it gives an Async<T>, and await e waits for e to complete and gives its T.
  • A function that returns an Async<T> it builds, without awaiting, is an ordinary function, as sleep(ms) -> Async<null> (std/async) is.
import "std/io"
import "std/async"

mk :: fn(n: i32) -> Async<i32> {
    await sleep(i64(n))
    return n * 10
}

main :: fn() -> i32 {
    rs :: await join_all([mk(3), mk(1), mk(2)])
    for r in rs {
        println("${r}")                  // 30, 10, 20: input order
    }
    return 0
}

await is an error in a nested lambda, a defer body and, for now, the right operand of &&, || or ??, a loop condition or a match guard.

Operands evaluate left to right even when a later one awaits: in f(a(), await b()), a() runs first. An assignment evaluates its target's operands (receiver, index), then its value; a compound assignment place op= e reads place only at the write.

defer works in an async function: the function's pending defers, those registered before an await included, run in reverse order when the function completes or is cancelled.

Running async code#

  • main may await and keep its i32 return type; the program runs it to completion.
  • When every task is suspended, the program waits for the nearest deadline, and stops on a deadlock when none is pending. Without std/async, a main whose await suspends stops the program.
  • sleep(ms) suspends the async function until its deadline: in real time in a native program, while under vader run the scheduler's clock jumps to it.
  • block_on(x) (std/async) runs an awaitable to completion from a function that is not async.

Combinators#

std/async combines awaitables:

  • join_all(xs) gives every result, in input order.
  • try_join_all(xs) is the fallible join, giving the first error in input order.
  • race(xs) completes with the first awaitable to settle, value or error.
  • join_any(xs) completes with the first success, or the last error when all fail.
  • timeout(ms, x) gives a TimeoutError when the deadline passes before x completes.
  • share(x) lets several awaiters await x: the first resume drives it, and once it completes every later one returns its result (a finished async function answers Suspended). Cancelling a shared awaitable leaves x to the others; x awaiting its own shared awaitable stops the program.

Cancellation#

race, join_any and timeout cancel the awaitables that lose, and nothing else cancels; only a suspended async function can be cancelled. It runs its pending defers in reverse order and cancels the awaitable it waits on, without finishing its body. Structured concurrency (scope, spawn) is planned.

Hand-written awaitables#

An awaitable is a value whose type implements Async<T> (std/core). Its resume returns the T once complete, or a Suspended while it cannot progress; one that holds resources overrides cancel to release them.

  • Suspended { .wake_at = t }: no progress before the scheduler clock (sched_now, in milliseconds) reaches t. Suspended {} (wake_at 0): the time is unknown.
  • An async function reports the wake_at of the awaitable it waits on.

join_all, race and join_any resume an awaitable only once its wake_at has passed.

Later: coroutines#

Continuation-passing coroutines are planned.

Native threads#

Native threads are planned after the MVP, in std/thread, whose import will be a compile-time error on the WebAssembly targets.