Two tills in a bakery sell from one stock room. Both read the shelf at the same moment: 50 loaves. Each sells 40. If you have written threaded code, you have met this bug, and its usual cure: a lock around every shared value, and the long work of getting every one of them right.

Cone's answer is to take the shared value away. The loaves belong to an actor: a worker that keeps its own data and handles the requests sent to it, one at a time.

actor StockRoom {
  loaves: i32 = 100;
  pub async fn take(self, wanted: i32) i32 {
    let given = if wanted <= loaves { wanted } else { loaves };
    loaves -= given;
    given                              // the answer, sent back
  }
}

actor Till {
  stock: StockRoom;
  pub async fn serve(self, wanted: i32) i32 {
    let got = await stock.take(wanted);
    got * price
  }
}

An actor is declared like a struct. Each async fn is a behavior: you call it like a method, and what happens is that a message goes into the actor's mailbox, to be handled when its turn comes. await waits for the answer, and the till's code reads top to bottom, though it runs on two actors and perhaps two cores. A shop can send both customers off at once and collect what they pay afterward:

let ana = east.serve(30);
let ben = west.serve(40);
print <- "Ana pays $", await ana, "\n";
print <- "Ben pays $", await ben, "\n";

Stock the shelf with 50 loaves and run it a few times. Which customer goes short can change from run to run, since either till may reach the stock room first. But between them they always pay for exactly 50 loaves, never one more.

Some work is not a set of standing roles but one big loop. Put parallel in front of an each, and every core works on it. This line tests every number below a million on all cores at once, and the list holds the primes in order, just as a plain loop's would:

let primes = List[u64]() <- parallel each n in 2u64 ..< 1000000u64 if isPrime(n) yield n;

No threads to start, no work to share out by hand. And the two mix. Here the bakery's ledger, an actor, takes ten percent off every large order at year's end, on all cores, from inside a behavior:

actor Ledger {
  orders: List[i32];
  pub async fn yearEnd(self) {
    parallel each o in orders.mutItems() {
      if *o >= 50 { *o -= *o / 10; }
    }
    // the loop has finished here: the same behavior carries on
  }
}

Why it's safe

A data race needs two threads reaching one value, with at least one of them changing it. Each of these rules takes one of those away, and the compiler checks them before the program ever runs.

An actor's fields are its own. Code outside that reaches for loaves is refused, with a message saying to send the actor a behavior instead. Inside, the fields are changed freely, since nothing else runs in that actor at the same time.

What travels in a message is checked from the behavior's own parameters. A number is copied. A value with a single owner moves whole, and the sender cannot touch it afterward. Data nobody changes may be shared by every actor at once. A value whose bookkeeping only one thread may keep is refused where the behavior is declared:

pub async fn sell(self, prices: Rc[imm, Prices], loaves: i32) i32 {
// Error 1194: Actor Till's behaviour sell takes prices, a Rc[imm, Prices], which is not
//   Sendable ... It may cross as a uni owner, which moves it, or as an owner of a region
//   declaring ThreadSafe, such as Arc.

And a parallel each body may only read what lies outside the loop. mutItems() gives each pass one order of its own to change, and no two passes the same one. Add up a total the risky way, and:

parallel each o in orders {
  total = total + *o;
}
// Error 1382: A 'parallel each' body may not write total, which is declared outside the
//   loop: the passes run at the same time. ... Hand results back with the parallel
//   builder, or 'mutItems()' on the items, or count with an Atomic.

Why it performs

Underneath is one pool of worker threads, one for each core. Whenever an actor has a message waiting, a free worker runs it. An await does not hold a core: the worker goes off to other work until the answer arrives. A value that moves into a message is handed over, not copied, and shared read-only data needs no lock, since the compiler has already proved nobody writes it.

A parallel each runs on the same workers. The loop starts as one piece; a busy worker cuts its piece in half and leaves one half for an idle worker to take, so the work spreads evenly even when some passes take longer than others. A sum such as primes.parallel().sum() is cut by the list's length alone, so its answer is the same on every run.

Why it matters as the code changes

An actor's state is reachable only through its behaviors, so its insides can change without hunting for outside users. A new behavior is one more async fn; nothing that already calls the actor changes, and there is no message type to extend. Each behavior's declaration also tells its callers whether an answer comes back, and the compiler refuses an await on one that sends none.

When a loop that was fast enough grows slow, making it parallel is one word. If its body was quietly depending on something outside the loop, the compiler names the variable, before the change ships. The same word works inside an actor: the behavior pauses where the loop ends, the actor's other messages wait their turn, and the rest of the behavior carries on once every pass is done. Work can move between a standing actor and an all-cores loop without rewiring either.

Try it

These links point at the Cone manual. The actors package runs on Windows and Linux (x86-64) today, not yet on macOS.

Why Cone · Previous: Shared, mutable data