await and futures in depth
On this page
Note: Not built yet: an await in a few places within an expression, a time limit on an await of an answer (an I/O operation's has one), cancelling a paused behaviour, waiting for whichever of several futures arrives first, and a way to look at a future without opening it.
An actor that needs another actor's answer must not hold a worker thread while it waits, or a few waiting actors would stall every core. So it awaits: the behaviour pauses, the actor goes on with its other messages, and the answer resumes the behaviour just after the await. The Guide showed the idea. This page says exactly what the pause costs and promises: which values wait, which borrows end, what happens to the rest of a statement, and what a future is. Afterwards you can write behaviours that wait without surprises. Here a page loads through a fetcher, which reads from the network:
mod fetch;
import stdio use *;
import actors;
// A connection: closing it prints a line
struct Conn {
pub url u64;
fn final(self &uni) { print <- " close ", url, "\n"; }
}
struct Buffer {
pub size u64;
}
actor Net {
pub async do read(self, url u64) Buffer {
print <- " read ", url, "\n";
new Buffer(url * 100u64);
}
}
actor Fetcher {
net Net;
pub async do fetch(self, url u64) Buffer {
let conn = new Conn(url); // opened
let buf = await net.read(conn.url); // conn waits, with the method
print <- " fetched ", buf.size, "\n";
buf; // the answer to the 'await' on fetch
} // conn closes here
}
actor Page {
fetcher Fetcher;
body u64;
pub async do load(self, u u64) {
print <- "load ", u, "\n";
let b = await fetcher.fetch(u); // pauses here
body = b.size; // and resumes when the answer comes
print <- "loaded ", body, "\n";
}
}
fn main() {
let page = new Page(new Fetcher(new Net()), 0u64);
page.load(7u64);
}
It prints:
load 7 read 7 fetched 700 close 7 loaded 700
Try this: send page.load(8u64) straight after the first load, and see how the two loads' lines interleave while each one waits.
Awaiting a behaviour's answer#
A message is one-way: the sender goes on at once, and whatever the behaviour returns is dropped. Inside an actor's own behaviour, await sends a message and waits for what the behaviour it sends returns.
When load reaches its await, it sends fetch to the fetcher and pauses. The worker running the page does not wait with it: the page goes back to its mailbox, and its other messages run meanwhile. When fetch returns its Buffer, the value comes back to the page as an answer, and load resumes just after the await, whose value it is. The fetcher does the same at its own await: while the network reads, the fetcher is free to handle other fetches. Its connection, conn, is still open while it waits, and closes when fetch ends, after the answer came, just as it would if read had been an ordinary call.
An I/O operation is awaited the same way, its answer coming back as a behaviour's does: a real read of a socket is await conn.read(buf) (Awaiting I/O).
What an await does#
- It asks. What follows await is a behaviour of an actor that returns a value. It is sent, as any message is, and the request carries where the answer goes. (Or it is a future, a request sent earlier: Futures.)
- The behaviour pauses. It returns to its actor's dispatcher. What the rest of the behaviour needs waits in the actor (below), not on a stack, so the worker goes on to other work.
- Other messages run. The actor handles the rest of its mailbox meanwhile, one message at a time, as always, messages it sends itself among them. So its state may have changed by the time the behaviour resumes: what it read from its fields before the await, it reads again after.
- The answer resumes it. When the behaviour asked returns, its value comes back to the actor that awaited, as a message in its mailbox. When that message's turn comes, the rest of the behaviour runs, with the value as the await's, perhaps on another worker thread.
Await only in a behaviour#
An await stands only in a behaviour. It may not stand in a plain function or a struct's method, nor in any fn of an actor's: its init, which runs on the thread making the actor, before the actor takes any message; its final, as it dies; or a helper, which runs inside the method that calls it and cannot pause partway through the call. A behaviour may hold any number of them.
That restriction is what keeps waiting cheap to write. Where any function may wait for an asynchronous answer, a caller that needs the result must become asynchronous itself, and its callers in turn, all the way up the stack: the waiting spreads through the program. The other common cure, threads that block while the runtime notices and runs something else, is slower and harder to get right, since noticing that a thread is blocked, and that it has woken, is neither cheap nor certain. An actor's behaviour needs neither. It is already at the top of its worker's stack, run by its actor's dispatcher, and every message already runs later than it was sent, so a caller never awaits a behaviour in order to call it. An await pauses only the behaviour it is written in, and goes no further. The behaviour's declaration says it may pause, and that is as far as the marking goes.
So the awaits go at the top of an actor's work, in its behaviours, and the synchronous work between them is pushed down, onto a helper or a type the actor owns:
actor Reporter {
clock Clock;
last u64;
pub async do report(self) {
let t = await clock.now(); // the 'await', at the top of the behaviour
last = self.format(t); // the synchronous work, pushed down
}
fn format(self &, t u64) u64 {
t * 1000u64 + last;
}
fn stamp(self) {
last = await clock.now(); // refused: stamp is a 'fn', synchronous
}
}
Error 1270: 'await' stands only in an actor's behaviour, declared 'async do', where the behaviour is cut and returns to the actor's dispatcher to wait. This one is in stamp, a 'fn' of actor Reporter's: synchronous, run inside the method that calls it, which cannot pause partway through a call. An 'await' belongs at the top of a behaviour: await there, and push the synchronous work down, onto a helper like this one or a type the actor owns, to run between the awaits.
That is also why a program's main, a plain function, starts its actors with a send and lets the waiting happen inside them, as the Guide's bakery does.
It reads as one scope#
The method runs in parts, with other messages between them, but it is still the one method written, with one scope. Every value it holds when it pauses dies where it would if the method had never paused, and in the same order:
struct Note {
pub n u64;
fn final(self &uni) {say(" drop", n);}
}
actor Clock {
pub async do now(self) u64 {42u64;}
}
actor Logger {
clock Clock;
pub async do log(self) u64 {
let first = new Note(1u64);
let second = new Note(2u64);
let t = await clock.now();
say(" at", t + first.n); // 'second' is not named again
t;
} // second dies, then first
}
(say prints a word and a number on a line, here and below.) await logger.log() prints at 43, then drop 2, then drop 1, and is 42. second is never named after the await, yet it waits too, and dies at the method's end, the last declared first, as it would in a method with no await. Nothing is dropped early at an await, so what a finalizer does (closing a file, giving back a buffer) happens where the method's text says. An await returns to the dispatcher, not to the author: the author reads one method, and the pause shows only in when the rest of it runs, and in what the actor's other messages did meanwhile.
What crosses an await#
- Values move. Every value the method still needs after the await, and every value with a finalizer it still holds, waits for the answer, and comes back to the same local when the method resumes. It is moved, never copied: a counted owner (an Arc, an actor's handle) is not counted again, and a value that may only move waits like any other. A local that is not used again and has nothing to do as it dies is left behind.
- Borrows end. Every borrowed reference the method holds ends at the await, as if its scope ended there: while the method is paused, other messages run, and may change or drop what was borrowed. Holding one at an await is no error; using one after it is (below).
- A 'static borrow does not end. A borrow of a global may already be returned from
a function, since what it points at outlives every function; for the same reason it crosses an
await:
let limit u64 = 100u64; actor Gauge { clock Clock; pub async do check(self) u64 { let max = &limit; // a borrow of a global: 'static let t = await clock.now(); *max - t; // still valid after the 'await': 58 } } - A lock is given back. A borrow through a lock permission holds its lock for
as long as the borrow lasts, so the borrow ends at the await and the lock is given back
there. To reach the value again, the method borrows it again:
struct Count {pub n u64;} actor Tally { count Arc[Mutex, Count]; clock Clock; pub async do bump(self) u64 { let c = &mut *count; // takes the lock c.n += 1u64; let t = await clock.now(); // gives it back (&mut *count).n += t; // takes it again, for the statement (&*count).n; // 43, from a count of 0 } }A lock borrow is never 'static, since it borrows a hidden local that holds the lock, so it never crosses: no method holds a lock while it waits. - A reference into the actor's own region is kept. The actor's collector keeps it: a paused behaviour's record, parked in the pending table, is one of its roots (The actor's own garbage-collected region). The collector package's Gc is refused inside an actor altogether (Inside an actor).
- The state is lent afresh. self and the actor's fields may be used after an await: the dispatcher lends the state again when the method resumes. A borrow made through self before the await ends, as any borrow does.
Using a borrow after an await#
A borrow used after the await that ended it is refused, with the error any borrow used after its end gets, pointing at the await as where it ended:
actor Meter {
clock Clock;
reading u64;
pub async do stale(self) {
let r = &reading;
let t = await clock.now(); // refused: r's borrow ends here, and r is used below
reading = *r + t;
}
pub async do fresh(self) {
let t = await clock.now();
let r = &reading; // borrowed after the 'await'
reading = *r + t;
}
}
Error 1168: The borrow 'r' holds of '*self' (made at 48:13) ends at this 'await', where the method returns to its actor's dispatcher to wait, and 'r' is used again at 50:16.
A lock borrow used after the await is refused the same way, the message naming the lock it held:
Error 1168: The borrow 'c' holds through the lock taken at 78:19 (made at 78:13) ends at this 'await', where the method returns to its actor's dispatcher to wait, and 'c' is used again at 80:5.
Await inside a statement#
An await may stand inside a larger expression. What follows await is worked out first, up to the request it sends; then the behaviour pauses, and the rest of the statement runs when the answer comes, as the first thing the behaviour does when it resumes:
actor Counter {
pub async do next(self) u64 {2u64;}
}
actor Shelf {
counter Counter;
slots Array[u64, 4];
total u64;
fn put(self, n u64) {total = total + n;}
pub async do stock(self) u64 {
slots[await counter.next()] = 7u64; // slots is reached after the 'await'
self.put(await counter.next()); // and so is self, put's receiver
total += await counter.next(); // total is read and written after it
slots[2] + total;
}
}
await shelf.stock() is 11. Each statement runs as if the value awaited were made first, into a local of its own: let i = await counter.next(); slots[i] = 7u64;. So what the rest of the statement reaches (slots, the state through self, total) it reaches as it is when the behaviour resumes, after whatever the actor's other messages did meanwhile, and nothing of it is borrowed while the behaviour waits. A borrow written to the left of the await is taken then too: add(&x, await clock.now()) borrows x once the answer has come, and total += await counter.next() loses nothing another message added to total while it waited. Holding the borrow across the pause instead would be safe only if nothing else could change what it reaches meanwhile, and inside an actor, its other messages run meanwhile.
A value is not reached again. One made to the left of the await, in the order the statement is worked out (an argument before it, an operator's operand), is made before the request is sent and waits with the method: in total + await counter.next(), total is read before the await. The value an assignment stores is made before its place is reached, so in slots[await counter.next()] = f(), f runs first.
What is reached again is a plain path: a local, self, a field, an element at a plain index, or a borrow of one. A borrow a call made cannot be made again, so a borrow, or a place, made by a call to the left of the await is refused, and the message says to bind what the call makes to a local first. In Meter, where add is a function taking a &u64 and a number, and compute one returning a number:
pub async do fromCall(self) {
reading = add(&compute(), await clock.now()); // refused: compute() made what is borrowed
}
pub async do bound(self) {
let c = compute();
reading = add(&c, await clock.now()); // c is borrowed after the 'await'
}
Error 1279: This borrow, written to the left of an 'await' in the same statement, is made by a call or a temporary, and is used after the 'await'. The rest of the statement runs after the 'await', which ends every borrow: only a plain path -- a local, 'self', a field, an element at a plain index -- is reached again there, never what a call made. Bind what the call makes to a local first, and use the local here.
More than one await may stand in a place: in both places a swap exchanges, in a slice's bounds, in the places of a parallel assignment, and among the entries a <- appends. Each is worked out first, from left to right, every await once: the indexes of the place, those of the second side of a swap after those of the first, and a parallel assignment's value before its places. An index worked out waits with the method while the ones after it are awaited, and the arrays and slices they index are reached after the last answer, as they are with one await. The entries of a <- written before the last one that holds an await are made before the list is borrowed, in the order written, and appended after it:
actor Lane {
pub async do pick(self, n u64) u64 {n;}
}
actor Board {
lane Lane;
cells Array[Array[u64, 3], 2];
marks List[u64];
pub async do fill(self) u64 {
cells[await lane.pick(1u64)][await lane.pick(2u64)] = 5u64; // cells[1][2]
cells[await lane.pick(0u64)][0u64] <=> cells[await lane.pick(1u64)][2u64]; // both sides
(cells[0][1], cells[await lane.pick(1u64)][await lane.pick(1u64)]) = (7u64, 8u64);
marks <- (cells[1][2], await lane.pick(9u64), 3u64);
let row = &cells[await lane.pick(1u64)][await lane.pick(0u64)..<await lane.pick(2u64)];
row.len() * 100u64 + cells[0][0] * 10u64 + *marks[1];
}
}
await board.fill() is 259.
One request, one answer#
An await sends one request and takes exactly one answer. A behaviour that returns a value answers once, when it returns, from whichever path it returns by:
actor Cache {
net Net;
hot u64;
pub async do get(self, url u64) u64 {
if url == hot {
return 1u64; // answered here, at once
}
let b = await net.read(url);
b.size; // or here, once the answer came
}
}
With 3 as the hot address, await cache.get(3u64) is 1 and reads nothing, and await cache.get(4u64) reads and is 400. An answer always has an actor to go to: the request holds a counted handle of the actor that awaited, so that actor cannot die while its answer is owed. A behaviour that returns a value may also be sent with no await, and its value is then dropped.
Futures: keeping a call's answer for later#
Awaiting each call as it is made has an actor wait for one answer before it even sends the next request. A call of a behaviour that returns a value, written where its value is kept (bound to a local, passed on, returned), sends the request at once and gives back a future: a value standing for what the behaviour will return. An await on the future waits for that value. So several requests can be in flight at once, and their answers collected later:
actor Stock {
pub async do price(self, item u64) u64 {item * 3u64;}
}
actor Basket {
stock Stock;
pub async do total(self) u64 {
let apples = stock.price(4u64); // sent: a future comes back at once
let pears = stock.price(5u64); // both requests are in flight
let a = await apples; // waits for the first
let p = await pears; // then for the second
a + p;
}
}
await basket.total() is 27. Both requests are sent before either is waited for, so the stock may work on the second while the basket waits for the first. A behaviour is declared once, and how it is called says what becomes of its value:
- await stock.price(4u64) sends it and waits here: the await's value is the behaviour's, and no future is made.
- let f = stock.price(4u64) sends it and keeps its future, an actors.Future[u64], for an await later.
- stock.price(4u64);, a statement of its own, sends it and forgets it: its value is dropped.
An await on a future pauses only if the value has not arrived. If it has, the behaviour goes on at once: nothing pauses, nothing waits in the actor, and none of the actor's other messages runs in between, so a future collected after other work is often free. If it has not, the behaviour pauses just as on an answer (What an await does), every borrow ending there, and resumes when the value arrives. Either way, what crosses the await is what crosses any await.
A future is a value like any other. It may be stored and awaited much later, and it may be handed to another actor in a message and awaited there: it belongs to whoever holds it, never to the actor that made the call. It crosses to another thread where its value may: Future[T] is Sendable where T is. It copies or moves as its value does. Future[u64] copies, and each await on a copy gives the value again. A future of a value that moves, a Future[Report] where Report is a move type, moves too: the await takes the value out of it, and the future goes with it, so no second await could take the value again:
struct Report is Move {
pub lines u64;
}
actor Printer {
pub async do render(self, n u64) Report {new Report(n * 2u64);}
}
actor Archive {
pub async do keep(self, f actors.Future[u64]) u64 {
let n = await f; // the value arrives here
n + 1u64;
}
}
actor Office {
printer Printer;
stock Stock;
archive Archive;
pub async do file(self) u64 {
let r = printer.render(10u64); // a Future[Report], which moves
let report = await r; // the Report is taken out of it
report.lines;
}
pub async do pass(self) u64 {
let f = stock.price(2u64);
await archive.keep(f); // the future goes to Archive
}
}
await office.file() is 20, and await office.pass() is 7. Awaiting r a second time is refused, as any use of a moved value is:
actor Desk {
printer Printer;
pub async do twice(self) u64 {
let r = printer.render(10u64);
let report = await r;
let again = await r; // refused: r moved into the first 'await'
report.lines;
}
}
Error 1048: This variable's value has been moved out. It is no longer there to use.
A future nobody awaits is no error and costs nothing more: the behaviour runs, its value arrives, and it is finalized when the last holder of the future lets go, or when the value arrives, whichever is later. Letting go of a future does not stop its behaviour, which is already running on its own actor.
An await assumes the value arrives, as it nearly always does, and hands it over directly, with no wrapper to unwrap. When the behaviour owing it can never answer (its actor died while the behaviour was paused on something abandoned itself, as at the program's end), the future is abandoned. A behaviour already paused on it is abandoned with it, as one paused on the request itself would be: the rest of it never runs, and the values it held are finalized, in the order its one scope gives, once nothing else holds its actor. So the program's end, which ends what is outstanding, ends let f = server.next(); await f as it ends await server.next(). An await that finds the future abandoned already panics, reported at the await:
panic in thread 25268 at concurrency_future_abandoned.cone:30: actors: an awaited future will never arrive: the behaviour owing it (request 1) ended without answering
The request number counts the futures the program has made, so the report says which one it was. Where the failure is one the program should handle, the behaviour declares it in its return type, a Result, and the future's value is that Result.
Only a future, a behaviour's answer and an I/O operation are awaited. An await on anything else waits for no answer, and is refused: an await on a function's call, whose value is there as soon as the call returns (below), or on a number. And a behaviour that returns nothing gives no future (next). The compiler's --await-direct option, which is for testing the cut, runs the rest of the behaviour at once after an await on anything else, with the value awaited.
A behaviour that returns nothing#
Whether an answer is sent is the behaviour's declaration to say, never its caller's: one with no return type sends none, so there is nothing to await, and an await on it is refused. A caller that needs to know a behaviour has finished needs to know whether it worked, too: the behaviour declares a return type, a status, and the caller awaits that.
actor Log {
pub async do flush(self) {} // owes its caller nothing
pub async do commit(self) bool {true;} // answers with a status
}
actor Writer {
log Log;
saved bool;
pub async do save(self) {
await log.flush(); // refused: flush sends no answer
saved = await log.commit(); // waits until commit has run, and learns whether it worked
}
}
Error 1274: 'await' waits for a behaviour's reply, and flush returns nothing, so it sends none: a behaviour with no return type owes its caller nothing. If the caller needs to know flush finished, declare a return type -- a status saying whether it worked -- and await that.
For the same reason its call gives no future: one whose value is kept is refused. Written as a statement of its own, it sends the message and forgets it.
actor Keeper {
log Log;
pub async do keep(self) {
log.flush(); // sent, and forgotten
let f = log.flush(); // refused: flush gives no future
}
}
Error 1282: flush returns nothing, so its call gives no future to keep: a behaviour with no return type owes its caller nothing, and calling it sends a message and forgets it. Call it as a statement of its own; or, if the caller needs to know flush finished, declare a return type -- a status saying whether it worked -- and keep or await that.
An acknowledgement is not flow control. A sender that awaits each answer before sending again has one message outstanding at a time, so the two run in lockstep, a round trip each. To keep a fast sender from running far ahead of a slow receiver while both stay busy, send through a bounded channel: its capacity is how far ahead the sender may get, and a send blocks only when it is full.
An await in a loop#
An await may stand in a loop, and a loop's variables cross it as any others do. It is legal, but poor practice:
actor Sizes {
net Net;
pub async do total(self, n u64) u64 {
var sum = 0u64;
each i in 0u64 ..< n {
let b = await net.read(i); // one request at a time
sum = sum + b.size;
}
sum;
}
}
await sizes.total(3u64) reads 0, 1 and 2, one after another, and is 300. Each pass sends one request and waits for its answer before the next is sent, so the requests run one after another, a round trip each, and the actor's other messages run in between the passes, perhaps changing what the loop reads. A borrow the loop holds ends at the await on every pass. Requests that do not depend on one another's answers need not wait in turn: keep each call's future, and they are all in flight at once, their values awaited afterwards (Futures).
Not built yet#
Each of these is reported where it is written, as not built:
- A few places an await may not cut yet: in the index of a place whose base is itself an expression holding an await ((await f()).items[await g()]), among the entries one <- appends that repeat or fill a value (v <- 3 of await f()), and in an array's contents that repeat a value more than 16 times (<- fill await f()). Written as a statement of its own first, each await is built.
- Timeouts and cancellation. A paused behaviour waits until its answer comes. There is no time limit on the wait, and no way to cancel a paused behaviour, except that an I/O operation it awaits can be limited and cancelled (Cancelling an operation, and limiting it). Only the program's end ends one early: a behaviour paused on I/O is abandoned then, the rest of it never run, and its values are finalized once nothing else holds its actor (The I/O thread, and the program's end); a behaviour whose answer was owed by one abandoned so is abandoned with it, and a future it owed is abandoned (Futures).
- Awaiting several futures at once. An await waits for one future. Waiting for whichever of several arrives first, or for all of them in one await, is not built: several are awaited one after another.
- Looking at a future without opening it. An await opens the future and panics if its value never arrives. There is no way yet to ask whether it arrived, or to handle an abandoned one, without the panic.
One of them, beside an await refused for what it awaits, where compute is a plain function:
actor Waiter {
clock Clock;
last u64;
pub async do onFunction(self) {
last = await compute(); // refused: compute's u64 is no future
}
pub async do repeated(self) {
var v = List[u64].empty();
v <- 3 of await clock.now(); // not built: among the entries a '<-' appends
}
}
Error 1281: 'await' waits for an answer: a behaviour's reply, a future a behaviour's call gave, or an operation. This one awaits a u64, which is none of them, so nothing is generated for it. ... Error 1271: 'await' is not built here yet, so nothing is generated for it: it stands among the entries a '<-' appends, whose receiver is borrowed once for all of them and would be held across the seam. Append the awaited value with a '<-' of its own.
You can now write behaviours that wait for other actors without holding a thread, keep several requests in flight with futures, and know what each pause does to the values and borrows a behaviour holds. The same await waits for a socket, a file or a timer, which is next.