burn / docs

Language

Async

Calling an async fun starts it in the background and immediately returns a Future<T>. await waits for the result.

import "std/time"

async fun download(name: string, ms: int): string {
    sleep(ms)
    return name + " done"
}

fun main() {
    var a = download("first", 200)
    var b = download("second", 200)
    print(await a)
    print(await b)
}

Both downloads run at the same time, so the program takes about 200ms instead of 400ms.

Tasks on their own threads, joined by await

Futures

  • An async fun returning T gives a Future<T> when called; one without a result gives a Future<void>, which can still be awaited to wait for it to finish.
  • Futures are values: store them in variables, arrays ([Future<int>]) or fields, pass them around, and await them later. Printing one shows <future>.
  • await f waits until the task is done and gives its result. Awaiting the same future again gives the same result at once.
  • await works in any function, not only in async ones: it simply blocks the caller until the result is there.
  • An async fun can be a top level function or a struct method (async fun save(): bool inside def struct). Async lambdas are not supported; wrap the work in a named async fun.

Starting many tasks and collecting their results:

async fun square(n: int): int {
    return n * n
}

[Future<int>] pending = []
for i in 0..5 {
    pending.push(square(i))
}

var total = 0
for f in pending {
    total += await f
}
print(total)

Running and ending

  • Every task runs on its own operating system thread, so tasks use several processor cores at once.
  • A task that is never awaited still runs to the end: the program waits for all running tasks before it exits.
  • A runtime error in a task stops the whole program with the error and exit code 1, as in the main program.
import "std/time"

async fun tick(name: string) {
    sleep(50)
    print(name)
}

tick("a")
tick("b")
print("main ends")

prints main ends first, then a and b once the tasks wake up, and only then exits.

Sharing data

Tasks share global variables and the objects passed to them. Memory management is safe across threads: reference counting switches to atomic operations while tasks run, so sharing objects never frees them too early. Changing the same array, map or object from several tasks at the same time is not synchronized, though; give each task its own data and combine the results after await:

async fun countWords(text: string): int {
    var n = 0
    for w in split(text, " ") {
        if (len(w) > 0) {
            n += 1
        }
    }
    return n
}

var parts = ["one two three", "four five", "six"]
[Future<int>] counts = []
for p in parts {
    counts.push(countWords(p))
}
var sum = 0
for c in counts {
    sum += await c
}
print(sum)

On each backend

bvm and native JavaScript
a call to an async fun starts a thread and returns at once runs the function to the end, then returns
await waits for the thread gives the result that is already there
sleep in a task sleeps only that task blocks the whole program

Results are the same on every backend; only the timing differs. Programs built with --no-std cannot use async, since threads are part of the standard runtime.

See running on bvm and memory management for how tasks work inside.