Skip to article
ALGORITHMICSConcurrency
Concurrency6 min read

Actors

One owner per piece of state, reachable only by message — and what that buys you across a network.


Every concurrency bug in the previous articles has the same root: two threads touching the same memory. Races, deadlocks, torn reads, stale caches — all of them need shared mutable state to exist.

The actor model removes the premise.

The rules

  1. An actor has private state. Nothing outside can read or write it.
  2. Actors communicate only by asynchronous messages.
  3. Each actor handles one message at a time, to completion.
  4. While handling a message it may change its own state, send messages, and create actors.

That is the whole model.

1 / 5
mailbox withdraw 30deposit 20withdraw 100balance?
actor deposit 50 150 private state

→ ok, 150

The balance is touched by exactly one thing, and that thing handles one message before starting the next. There is no lock because there is nothing to exclude — concurrency exists between actors, never inside one.

What it looks like

class Account {
private balance = 100; // private, and means it
async receive(message: Message) {
switch (message.kind) {
case 'deposit':
this.balance += message.amount;
break;
case 'withdraw':
if (message.amount > this.balance) {
message.replyTo.send({kind: 'declined'});
return;
}
this.balance -= message.amount;
message.replyTo.send({kind: 'ok', balance: this.balance});
break;
}
}
}

No lock, no atomic, no volatile. A thousand concurrent transfers are safe by construction.

The awkward parts

Two actors are not atomic together. Moving money between two accounts is two messages, and the system is observable in between. You need a third actor to run the transaction, or a saga, or to accept eventual consistency. This is the same problem distributed systems have, which is not a coincidence — see below.

A slow actor is a bottleneck. One message at a time means one core, per actor. The answer is more actors — one per account rather than one per bank — and getting that granularity wrong is the usual performance mistake.

Unbounded mailboxes hide overload. A producer faster than its consumer grows the mailbox until memory runs out. Bound it and apply backpressure.

The part that makes it interesting

Because actors only exchange messages, it does not matter where they run.

The same code works with both actors on one thread, on two threads, or on two machines. There is no shared memory to lose, so nothing breaks when the address space does.

Where to find it

Erlang and Elixir (built in), Akka and Pekko on the JVM, Microsoft Orleans, Swift’s actor keyword, and Web Workers, which are actors whose messages must be serialisable.

The closest relative is CSP: both avoid shared state by passing messages. The difference is what is named — actors have identities and mailboxes, CSP has anonymous processes and named channels.