Actors in depth
On this page
Note: The actors package runs on Windows and Linux (x86-64), not yet on macOS. Not built yet: actor trait, a generic actor, and a queued method on a message type.
The Guide's bakery gave each part of a program an actor of its own, and let them talk. Building something real raises the questions the tour skipped: when is an answer sent back, when does an actor wait, how is one made and when does it die, and what may it hold? This page has the whole of it. Afterwards you can design an actor's behaviours, its start and its end, knowing exactly what each one costs. It begins with the four shapes a behaviour can take, all in one program:
mod shapes;
import stdio use *;
import actors;
actor Store {
total u64;
// Fire and forget: no return type, no await
pub async do add(self, k u64) {
total = self.plus(k);
}
// A cheap query: a return type, no await
pub async do count(self) u64 { total; }
// Synchronous: an ordinary call, inside the actor
fn plus(self &, k u64) u64 { total + k; }
}
actor Client {
store Store;
// Awaits, owes nobody: an await, no return type
pub async do run(self) {
store.add(5u64);
store.add(7u64);
let c = await store.count();
print <- "count ", c, "\n";
let r = await self.report();
print <- "report ", r, "\n";
}
// The full handshake: an await and a return type
pub async do report(self) u64 {
let c = await store.count();
c * 10u64;
}
}
fn main() {
let client = new Client(new Store(0u64));
client.run();
}
It prints, on every run:
count 12 report 120
Try this: move store.add(7u64) to after the await in run, and predict the count before you run it.
Four shapes of behaviour#
Two things about a behaviour are independent, and each decides one thing. Whether it declares a return type decides whether an answer is sent back; whether it holds an await decides whether it may pause partway. So every behaviour has one of four shapes, and you can tell which from its declaration alone:
- Fire and forget: no return type, no await. It runs once and ends; nothing is sent back, and nothing waits. The cheapest shape, and Store.add's.
- A cheap query: a return type, no await. It runs once and answers with its value as it ends, to a caller that awaited it: Store.count.
- Awaits, owes nobody: an await, no return type. It waits for other actors' answers, and answers no one: Client.run.
- The full handshake: both. It waits for answers, then answers with its own value: Client.report, which run awaits in turn.
The count is 12 every time, never 5 or 0, because one sender's messages to an actor are handled in the order they were sent: both adds reach the store before the count does. Choose the cheapest shape that does the job. An answer costs a message back, and an await costs a pause, which Awaiting and futures takes apart in full.
Behaviours and synchronous methods#
A method declared async do is a behaviour: it runs asynchronously, when the actor is scheduled, and an await on one waits for its answer. async do is one keyword of two words, and neither means anything alone. async says when the behaviour runs, and pairs with await; do is the declaration's verb, as fn is a function's. Written without the other, either is refused:
Error 1273: 'async' means nothing alone: an actor's behaviour is declared 'async do name(self, ...)'.
A behaviour never starts on the sender's thread, as an asynchronous function may in some designs: a send only queues the message, so no thread but the one running the actor ever touches its state.
Synchronous methods#
A method declared fn is synchronous: the actor's init and final, and any helper, such as Store.plus. It sees the actor's fields directly and runs inside the method that calls it, as a struct's method does. It is the actor's alone: never pub, and not carried by the handle. It holds no await. An await belongs at the top of a behaviour, and the synchronous work between awaits is pushed down, onto a helper or onto a type the actor owns (Await only in a behaviour).
The declaration#
An actor is declared with actor, in a module that imports the actors package: the mailbox, the work-stealing scheduler, the counting that decides when an actor dies, and the quiet the program ends at. Its body is written as a struct's: fields, an init, behaviours, synchronous methods, a final:
actor Counter {
count u64; // its state: private to it
fn init(self &new, start u64) { // run by 'new Counter(start)'
*self = new Self(count: start);
}
pub async do add(self, n u64) { // a behaviour: sent as a message
count = count + n;
}
pub async do report(self &, to Printer) { // carries another actor's handle
to.show(self.doubled());
}
fn doubled(self &) u64 { // synchronous: called, inside the actor
count * 2u64;
}
fn final(self &uni) {} // when it dies
}
actor Printer {
pub async do show(self, n u64) {
print <- "doubled: ", n, "\n";
}
}
fn main() {
let c = new Counter(0u64); // the first actor starts the workers
c.add(5u64); // a message: queued, and run later
c.add(7u64); // run after the first
c.report(new Printer()); // prints doubled: 24
}
The rules of the declaration:
- The fields are the actor's state, private to it: a field is never pub, and nothing outside the actor reaches one.
- A method's self is the state, lent to a behaviour for the message it handles, and to a synchronous method by its caller. Written bare, it is self &mut, which the method has alone while it runs; self & only reads it.
- A behaviour that is not pub is a message all the same, which only its own module sends. A function without self is the actor's own, called from its methods as a struct's is.
- A behaviour is not generic, and every parameter it takes is Sendable, since the
message carries its arguments to the thread the actor runs on
(What may cross a thread). A borrow is refused, unless it lasts the whole program,
is written so (&'static imm T, &'static opaq T or
&'static uni T) and reaches what may cross
(A borrow that lasts the whole program). The refusal says what to
pass instead:
Error 1194: Actor A's behaviour take takes r, a borrowed reference, which is not Sendable: no borrow may leave its thread. A message carries its arguments to the thread the actor runs on; pass an owner that may cross, such as a 'uni' one, or an 'Arc', or a borrow that lives for the whole program, written &'static imm, &'static opaq or &'static uni.
- A behaviour may return a value, which must be Sendable too, by the same rule, so a borrow in it is written 'static as a parameter's is. A plain send returns at once, before the behaviour runs, and the value is dropped, unless the behaviour was awaited or its future kept (Awaiting and futures).
- An actor declares no static, macro, extern method or use, no type parameters, and names no abstraction. actor trait, its abstraction, is not built yet.
Making an actor#
new Counter(args) makes an actor and answers its handle. The actor is real before its init runs: the runtime's part of it is made first, and the init then runs, on the calling thread, on the state where it will stay. So the init, and any synchronous method it calls, may send to the actor itself, once *self = ... has filled the state. Before that, self holds nothing, and every use through it is refused, as in any initializer; after it, a send may stand anywhere, in an if, a loop or a match arm too, and the init may hand selfactor to another actor. The actor runs no message until its init has returned: a message sent meanwhile, by the init or by another actor already given the handle, waits in the mailbox and runs afterwards, in the order sent:
actor Starter {
count u64;
fn init(self &new) {
*self = new Self(count: 0u64);
self.note(1u64); // a send to itself: queued, run after the init
self.helper(); // a helper the init calls may send too
print <- "init ends ", count, "\n";
}
fn helper(self) {
selfactor.note(2u64);
}
pub async do note(self, k u64) {
count = count + k;
print <- "note ", count, "\n";
}
}
new Starter() prints:
init ends 0 note 1 note 3
Both notes wait until the init has returned. A panic in the init is a panic, which ends the program.
The initializer's parameters must be Sendable too, since the state made from them runs on other threads. An actor may declare several initializers, as a struct may, each joining the overload name init. One that declares none is made by its state's implicit initializer, as the bakery's were: new Store(0u64) takes the fields whose types are written, in order, a field with a default taking it where the argument is left out. That initializer is the module's own, as a struct's implicit one is where its fields are private, except for a pub actor with no fields at all. There is no other way to make an actor, and no creation function: spawn stays reserved.
The handle#
The actor's name names its handle: a value holding one counted owner of the actor's mailbox, the runtime's part of it. Copying a handle (passing it, storing it, putting it in a message) makes another owner, and each going drops one. A handle carries the actor's behaviours and nothing else: calling one on it, c.add(5u64), sends that message, and reaching a field of the state or a synchronous method through it is refused as private. A handle is Sendable whatever the state holds: it may be sent to a thread, over a channel, in another actor's message, or kept in another actor's state. So the state may hold what may not cross threads (an Rc, a mut owner, borrows inside a method), since it never crosses as a value: only the runtime reaches it.
Messages and their order#
A send is one-way: it queues the message and returns. An answer comes back only to an await (Awaiting a behaviour's answer), or as a message sent back to a handle passed along, as report sends show to the printer above. Each sender's messages to one actor are handled in the order sent, and a message is never overtaken by one it caused. Messages from different senders interleave in whatever order they arrive, which is why the Guide's bakery could not say which customer would go short.
Each argument arrives moved into the behaviour's call, once. A value that may only move (a uni or Uni owner) arrives whole, and a counted one (a handle, an Arc, a lock-managed Arc[Mutex, T]) is counted once.
An actor's life#
The actors package starts and stops by itself. The first actor made starts the worker threads, and when main returns, the module's finalizer waits until all is quiet, then stops them (How the runtime works has the details).
While an actor is queued to run, or running, the scheduler holds one owner more, taken by the send that found it idle and dropped when its mailbox is found empty. So an actor dies exactly once, when its last handle has gone and its mailbox is empty. A handle dropped with messages still queued leaves the actor alive until they are handled. At its death its state is finalized (its final, then its fields, handles to other actors among them), on whichever thread let go last, then the block is freed. Actors that hold handles to each other in a cycle never die by counting, as a cycle of Arc owners never does, until one of them lets go.
self and selfactor#
An actor has two selves. self is its state, lent to the method while it runs; it never leaves the actor. selfactor is its handle: another owner of the actor, a value that may go anywhere a handle may: into a message, into another actor's state, or to an await on one of its own behaviours:
actor Doubler {
pub async do double(self, n u64) u64 { n * 2u64; }
pub async do twice(self, n u64) u64 {
let once = await selfactor.double(n); // a message to itself
let again = await selfactor.double(once);
again;
}
}
await doubler.twice(5u64) is 20. Each double is queued in the doubler's own mailbox, and runs because twice has paused and the actor is free. selfactor may be written in any of the actor's methods, its init among them once the state is filled, but not in its final, nor anywhere outside the actor.
Sending to itself#
In the actor's own methods, self.m(args), where m is one of its behaviours, sends m to the actor itself, as selfactor.m(args) does: the message joins the end of the actor's own mailbox, the method goes on at once, and m runs later. A behaviour is never called, not even by its own actor: it runs only when its dispatcher takes up a message for it. A synchronous method is called, as a struct's is:
actor Echo {
pub async do start(self) {
print <- "start\n";
self.later(1u64); // a send to itself: start goes on at once
self.relay(); // a synchronous helper, an ordinary call
print <- "start ends\n";
}
async do later(self, k u64) {
print <- "later ", k, "\n";
if k == 2u64 {
self.finish();
}
}
fn relay(self) {
later(2u64); // 'self.later(2)': a send, from a helper too
}
async do finish(self) {
let d = await self.double(21u64);
print <- "finish ", d, "\n";
self.show(d / 2u64 - 11u64);
}
async do double(self, k u64) u64 { k * 2u64; }
async do show(self, k u64) {
print <- "show ", k, "\n";
}
}
Sent echo.start(), this prints:
start start ends later 1 later 2 finish 42 show 10
Both lines of start come before either later runs. await self.double(21u64) is a send and an await, as through selfactor, never a call run at once. A behaviour reached through anything but the method's own self (another borrow of a state, a parameter's, say) is refused, and so is one sent through self in the actor's final, as it dies. In its init it is a send like any other, run once the init has returned (Making an actor).
Private state#
The state exists only inside its actor, and the compiler keeps it there. It is constructed in one place, as the actor is made, where its init fills it, *self = new Self(count: 0u64). Any other construction of it, new Self(...) in one of the actor's methods or as an allocation's value, by its fields' names or by its init, is refused, since the state it made would be in no actor, and the sends it or a helper made to its actor would reach nothing. Nor is the state copied out of its actor: it is a move type whatever its fields are, so let s = *self is refused, as taking any value out through a borrow is. And nothing outside reaches a field through the handle:
actor Keeper {
count u64;
pub async do start(self) {
var s = new Self(count: 5u64); // refused: a state in no actor
}
pub async do peek(self) {
let s = *self; // refused: the state is not copied out
}
}
fn main() {
let k = new Keeper(0u64);
let n = k.count; // refused: private state
}
Error 1290: Actor Keeper's state exists only inside its actor, so it is constructed only as the actor is made
('new Keeper(...)'), where its init fills it, '*self = new Self(field: value)'. Constructed here, it would be a
state in no actor, and a send it made to its actor would reach none. For a value of this shape, declare a plain
struct.
Error 1120: May not move a value out through a borrowed reference, which does not own it.
Error 1042: `count` is part of actor Keeper's private state, which only its own methods reach, one message at a
time. Send it a behaviour that uses it.
For a value of the state's shape, declare a plain struct.
Inside an actor#
A method of an actor may use what a single thread may: borrows, and Uni and Rc owners, whose count need not be atomic, since one worker at a time runs an actor and every hand-off of an actor between workers orders what the first did before anything the next does. Three things differ from a plain thread:
- The collector package's Gc is not supported inside an
actor (it is discouraged everywhere, and likely to be replaced). That collector
is single-threaded, and an actor runs on every worker in turn, so a traced reference made, held, reached or
dropped by an actor would corrupt it. The compiler refuses one made or held in an actor: a field of its state, a
synchronous method's parameter, an allocation, a call that returns one, or a literal holding one in any method's
body, init and final included, and a struct, tuple, array or enum
holding one as well. The message says what to do instead:
Error 1389: A traced reference (a Gc, for example) cannot be used inside an actor or a parallel each: this makes a Gc[mut, u64], which a collector traces, inside an actor's method. That collector's one chain of roots and its heap are process globals, not safe on the threads an actor or a parallel each runs on. Use Gc only in code that runs on a single thread; in an actor, allocate with 'new self[T](...)' and hold an ActorGc (the actors package's), which the actor collects between its messages.
An actor has a garbage-collected region of its own, whose references it makes, holds and copies freely (The actor's own garbage-collected region). - A thread-local global seen from an actor's method is the current worker's copy, not the actor's: its next message may run on another worker.
- A method that blocks holds its worker while it does. Actors wait by being sent a message, or by awaiting: for another actor's answer, or for I/O.
What the declaration stands for#
Underneath, the compiler turns an actor into four ordinary declarations, which the actors package runs:
- The state, a struct of the body as written, named Counter.State.
- The messages, an enum with one variant per behaviour, named for it, its fields the behaviour's parameters, named Counter.Msg.
- The handle, Counter, whose one field is an Arc[opaq, actors.Mailbox[Counter.Msg]]. It has an initializer for each of the state's, which makes the actor's block with actors.makeActor, constructs the state in it in place, and starts the actor with actors.startActor; and a method per behaviour, as public as the behaviour, which calls actors.send.
- The dispatch function, Counter.dispatch, which the runtime hands each message where it lies, and which moves each argument out of it into the behaviour's call.
Those names are what a diagnostic about them says; no source can write them, so nothing but the actor reaches its state, its messages or its mailbox. The mailbox and the state are one allocation, so a send touches one block. Actors and the declarations that use them may be written in any order: a message enum holds handles by value, a handle reaches the enum only through its owning reference to the mailbox, and a reference never needs what it points at to be laid out first.
You can now choose each behaviour's shape, make an actor and know when it dies, and keep its state its own. Some work is not a standing role at all but one big loop; next, the parallel each that runs it, in full.