skip to content

Message Passing and Isolation

Avoiding shared mutable state entirely: communicate by sending messages over channels, into actor mailboxes, or through bounded buffers. Interviewers raise it as the design alternative once you've enumerated locking's hazards.

part ofComputer science fundamentalsoverview, primer and where to startread it →
on this pageshow

questions

21

Describe the actor model of concurrency: what an actor consists of, and why processing one message at a time removes the need for locks around an actor's state.

level: juniorimportance: must knowfreq 50%

answer

  1. private state + mailbox + behaviour
  2. one message at a time = mutual exclusion by construction
  3. only three actions: send, create, change behaviour
  4. no shared memory, asynchronous send, never block a handler
  5. millions of actors multiplexed onto a small pool

basics

~20 s

An actor is an isolated unit with private state, a mailbox, and behaviour. Actors never share memory; they only send each other asynchronous messages. Each actor processes its mailbox strictly one message at a time, so its state is never touched concurrently and needs no lock.

solid answer

~60 s

An actor has three parts: **private state** no other actor can reach, a **mailbox** (an incoming message queue), and **behaviour** — the code that handles one message. Actors communicate only by sending asynchronous messages: send is fire-and-forget, the sender does not block or wait for a result. The key invariant is **single-threaded message processing**: an actor handles exactly one message to completion before starting the next. So although thousands of actors run concurrently across a small pool of threads, any individual actor's state has only one thread of control touching it. Mutual exclusion is structural rather than something you remember to write, so there are no locks to forget, no lock ordering to get wrong, and no lock-based deadlock inside an actor. In handling a message an actor may do three things: send messages to actors it knows, create new actors, and change its own behaviour/state for the next message. The cardinal sin is blocking inside a handler — the actor cannot process anything else while blocked, and it occupies a pooled thread.

code

text · 8 lines
text
actor Counter:
  state count = 0                 // reachable only here

  on message Increment(n):
     count = count + n            // no lock: one message at a time

  on message GetCount(replyTo):
     send replyTo, Count(count)   // asynchronous, no return value

go deeper

for a junior

Give the three parts and the one-message-at-a-time rule, and conclude that private state plus serialized processing means no locks.

for a middle

Add the scheduling picture — many actors multiplexed over a small pool — and why blocking inside a handler is harmful; mention request/reply being modelled as two messages.

for a senior

Discuss what the model trades away: per-actor serialization as a throughput ceiling, lost stack traces, and hazards moving to delivery guarantees and mailbox growth.

for a principal

Position actors against alternatives (channels, stateless workers plus a database) and discuss when entity-per-actor modelling with sharding and supervision is worth its operational complexity.

## The model The actor model is a concurrency model in which the unit of computation is an **actor**: an entity with state that nobody else can touch, an address other actors can send to, and a mailbox where those messages queue up. There is no shared memory between actors, no shared mutable objects, and therefore nothing to lock. An actor consists of: - **Private state** — variables owned exclusively by this actor. Not reachable by reference from outside; not guarded by a lock because it does not need one. - **A mailbox** — a queue of incoming messages, filled asynchronously by any sender. - **Behaviour** — a handler that processes one message and may change what the handler does next. In response to a single message an actor can do exactly three things (the classic formulation): **send** a finite number of messages to addresses it knows, **create** a finite number of new actors, and **designate** the behaviour it will use for the next message. Everything an actor system does is composed of those primitives. ## Why no locks are needed The defining rule is that an actor processes **one message at a time, to completion**. The runtime guarantees it never runs a given actor's handler on two threads simultaneously. Since the state is reachable only through the handler, and the handler is effectively single-threaded, every classic shared-state hazard inside the actor disappears: no data race on the state, no torn read of a two-field invariant, no need to reason about which lock protects which field. This is mutual exclusion by construction. It is worth stating the contrast: with threads and locks, correctness depends on every programmer remembering to take the right lock in the right order every time; with actors, the runtime enforces serialization and there is nothing to forget. The failure modes move elsewhere (overflow, ordering, protocol errors) but the classic race is designed out. ## Concurrency without a thread per actor Actors are cheap because they are not threads. A runtime multiplexes millions of actors over a pool sized near the core count: when an actor has messages, the scheduler picks it up, runs the handler for one message (or a small batch), then releases the thread. An idle actor consumes memory for state and mailbox, and nothing else. That model has a hard requirement: **handlers must not block**. If a handler waits on a synchronous network call, a lock, or a sleep, it holds a pool thread while doing nothing and the actor's own mailbox stalls behind it. Real systems either forbid blocking in handlers or isolate blocking work onto a separate, dedicated pool. ## What the model gives you beyond lock-freedom - **Location transparency.** Since interaction is only by message to an address, the recipient can be in-process or on another machine. That is what makes actor runtimes a natural distribution fabric — with the caveat that remote sending has failure modes local sending does not. - **Encapsulated failure.** An actor that crashes takes its own state down, not the process; a supervisor decides what to do next. - **Natural modelling.** Anything with identity and state over time — a connection, a game entity, a shopping cart, a device — maps directly onto an actor. ## Where actors are not magic Serialization is per actor, so a single actor is a single-threaded bottleneck: throughput on one entity is capped by one core, and hot entities must be sharded across many actors. Asynchronous messaging also loses the call stack, so 'who caused this' becomes a tracing problem rather than a stack trace. And the ordering and delivery guarantees are weaker than a method call — a topic in its own right. ## Interview delivery Name the three parts (private state, mailbox, behaviour), state the one-message-at-a-time rule and derive lock-freedom from it, mention that actors are multiplexed over a small thread pool so blocking in a handler is the cardinal sin, and close with a one-line note that the hazards move to message ordering, delivery and mailbox growth rather than vanishing.

  • If actors never share memory, how does one actor get a result from another?
    By protocol, not by return value. The requester includes its own address in the message, and the responder sends a reply message back; the requester correlates it with the original request and applies a timeout, since no reply may ever arrive. Many runtimes wrap this as an 'ask' that yields a future, but underneath it is still two one-way messages.
  • What happens if a message handler performs a blocking network call?
    The actor cannot process any other message until it returns, so its mailbox backs up, and because actors share a small thread pool it also removes a worker thread from the whole system. Enough blocked handlers starve unrelated actors. The remedy is to make the call asynchronous, or to run blocking work on a separate dedicated pool and deliver the result back as a message.

An actor is a clerk in a private office with a single in-tray. Nobody may reach into the office; you slide a request into the tray. The clerk handles one slip at a time, so the files on the desk are never contested.

saying these in an interview costs you the question

  • Believing every actor has its own OS thread
  • Assuming send blocks until the recipient processes the message, like a method call
  • Sharing a mutable object by putting a reference to it in a message and continuing to modify it
  • Thinking actors eliminate all concurrency bugs rather than trading them for ordering, delivery and overflow issues
  • Doing blocking I/O or long computation inside a handler

context

open as a page

What is the CSP (Communicating Sequential Processes) model of concurrency, and how does coordinating tasks by passing values over channels differ from coordinating them with shared mutable state and locks?

level: juniorimportance: must knowfreq 50%

basics

~20 s

CSP models a program as independent sequential processes that share no memory and interact only by sending and receiving values on channels. The communication is itself the synchronization, so there is no shared mutable state to lock.

open as a page

When concurrent tasks exchange data as messages, what properties must the message type have for any number of receivers to read it safely without coordinating, and what does immutable have to mean for a value that contains other objects inside it?

level: juniorimportance: must knowfreq 55%

basics

~20 s

The message must be fully built before it is sent and never change afterwards, all the way down: nested objects, arrays and collections must be immutable too, and the sender must keep no reference it can mutate. Then every reader sees identical content regardless of timing.

open as a page

Describe the producer-consumer pattern built around a bounded buffer: which threads block, under what conditions, and what problem the arrangement solves that a direct call would not.

level: juniorimportance: must knowfreq 68%

basics

~20 s

Producers put work items into a shared fixed-size buffer; consumers take them out. A producer blocks when the buffer is full, a consumer blocks when it is empty. This decouples the two sides in time and rate while the fixed capacity stops a fast producer from exhausting memory.

open as a page

What message ordering and delivery guarantees do actor systems typically provide, what do they explicitly not provide, and how does that change the way you write message handlers?

level: middleimportance: must knowfreq 42%

basics

~20 s

Typically at-most-once delivery, with FIFO order preserved only per sender-receiver pair. There is no global ordering, no causal ordering through intermediaries, and no guaranteed delivery. So handlers must tolerate lost, delayed and interleaved messages, and be idempotent if you add retries.

open as a page

In a channel-based concurrency model, what is the difference between an unbuffered (rendezvous) channel and a buffered one, and what does each guarantee about when the sender resumes?

level: middleimportance: must knowfreq 55%

basics

~20 s

An unbuffered channel is a rendezvous: the send completes only when a receiver takes the value, so both sides synchronize. A buffered channel lets the sender resume as soon as the value fits in the buffer, and blocks only when it is full.

open as a page

What does it mean to confine mutable state to a single task or thread, what forms does confinement take in practice, and why does confined state need no synchronization?

level: middleimportance: must knowfreq 48%

basics

~20 s

Confinement means only one task can ever reach a piece of mutable state, so concurrent access is impossible and no locking is needed. It comes as local variables, per-task storage, a single owner task, or one owner per data partition — and it holds only while no reference escapes.

open as a page

You are implementing a fixed-capacity buffer using a mutex plus condition variables (wait/signal). Why must a waiting thread re-check its condition in a loop rather than a single if, and when does waking only one waiter cause the system to hang?

level: middleimportance: must knowfreq 52%

basics

~20 s

Between being woken and re-acquiring the lock, another thread may have changed the state again, and some runtimes allow spurious wakeups — so the condition can be false on wake. Always loop. Waking one waiter hangs the system when all waiters share one condition variable and the wake goes to a thread of the wrong kind (producer wakes producer), losing the notification.

open as a page

A program uses channels exclusively and holds no locks at all, yet it hangs. What kinds of channel usage cause that, and how do you diagnose and prevent it?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Channel operations block, so tasks can wait in a cycle just like lock holders do. Typical causes: a send with no receiver ever arriving, a receive with no sender, two tasks sending to each other over unbuffered channels, and a loop waiting for a stream nobody closes.

open as a page

When a task must wait on several channels at once, what does a select (multi-way choice) construct do, and what happens if more than one channel is ready at the same moment?

level: middleimportance: should knowfreq 42%

basics

~20 s

Select waits on several communication operations at once and proceeds with whichever becomes ready, running only that branch. If several are ready it picks one nondeterministically, usually at random for fairness. An optional default branch makes it a non-blocking poll.

open as a page

Explain the copy-on-write technique for sharing a data structure between many concurrent readers and an occasional writer: what invariant lets readers run without locking, and what is its cost model?

level: middleimportance: should knowfreq 40%

basics

~20 s

Readers read a snapshot that is never modified in place. A writer copies the structure, changes the copy, and atomically swaps the shared reference. Readers need no lock and see a consistent, possibly slightly stale, version. Cost: a full copy per write, so only for read-mostly data.

open as a page

Show how a fixed-capacity buffer can be built from two counting semaphores plus a mutex, and explain what goes wrong if a producer acquires the mutex before the semaphore that counts free slots.

level: middleimportance: should knowfreq 42%

basics

~20 s

Use one semaphore counting free slots (starts at capacity) and one counting stored items (starts at zero), plus a mutex for the buffer internals. A producer acquires a free slot, locks, inserts, unlocks, then releases an item permit; the consumer mirrors it. Locking before acquiring the slot deadlocks: the producer sleeps holding the mutex, so no consumer can ever free a slot.

open as a page

An actor receives messages faster than it can process them and its mailbox keeps growing. What strategies exist for handling mailbox overflow, and how do you choose between them?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Options: unbounded mailbox (risks memory exhaustion), bounded with drop-newest, drop-oldest or priority-based dropping, bounded with rejection back to the sender, or credit-based flow control where senders only send what the consumer has requested. Choose by whether the messages are droppable, and fix the throughput mismatch as well.

open as a page

Explain supervision trees and the 'let it crash' philosophy in actor systems: who handles a failed actor, what options a supervisor has, and what this buys compared with catching every exception in place.

level: seniorimportance: should knowfreq 38%

basics

~20 s

Each actor has a parent that supervises it. On failure the child stops and its parent decides: restart it to a known-good state, resume it, stop it, or escalate to its own parent. Recovery lives in the tree, not scattered through handlers, so failure is isolated and state cannot stay corrupt.

open as a page

In a channel-based pipeline, how should closing a channel work: who is allowed to close it, what do receivers observe afterwards, and what breaks when several producers share one channel?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Closing broadcasts end-of-stream. Receivers drain any buffered values first, then every receive reports closed immediately. Only the sending side may close, and with several producers you need a coordinator that closes once all of them have finished, never each producer closing.

open as a page

Copying is too expensive, so one task must hand a large mutable buffer to another instead. What rules make that handoff safe, and how do you make I no longer own this something stronger than a comment?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Keep the rule that exactly one task may touch the buffer at any time: the sender finishes all writes, hands it over, and drops its reference for good. Use a handoff primitive that also provides ordering, and enforce ownership with move semantics, a detached source, or a one-shot handle — not a comment.

open as a page

A pipeline has several producer threads feeding a fixed-capacity queue and several consumer threads blocked in a blocking take. How do you shut it down cleanly so every queued item is processed and every consumer exits? Describe the sentinel or "poison pill" technique and the cases where it fails.

level: seniorimportance: should knowfreq 40%

basics

~20 s

A flag alone cannot work — blocked consumers never see it. Enqueue a special sentinel value after the last real item; a consumer that takes it stops. With N consumers you must enqueue N sentinels (or have each consumer re-enqueue the one it took), and only after every producer has finished, so nothing follows the sentinel.

open as a page

One stage of a channel pipeline is the bottleneck. How would you fan the work out to parallel workers and fan the results back into a single stream, and what must you decide about ordering, buffer sizes, and shutdown?

level: principalimportance: should knowfreq 33%

basics

~20 s

Fan-out: run N copies of the stage all receiving from the same input channel, so idle workers naturally take the next item. Fan-in: a merger that forwards each worker's output into one channel and closes it after all workers finish. Fan-out loses ordering; keep buffers small and give every worker a cancel path.

open as a page

You are designing a service where every request mutates some account's state. Compare a share-nothing design, where state is partitioned so exactly one task owns each account, with a shared-state-and-locks design. What do you gain, and where does share-nothing break down?

level: principalimportance: should knowfreq 33%

basics

~20 s

Share-nothing routes each account to its single owner, so updates are serialized per account with no locks, cache-friendly and easy to reason about. It breaks on hot partitions, on operations spanning two accounts, and on ownership changes, which need fencing to prevent two owners.

open as a page

How do you decide the capacity of a work queue sitting between producers and consumers, and what changes about the system's behaviour if you make that queue effectively unbounded?

level: principalimportance: should knowfreq 46%

basics

~20 s

Capacity is a latency and memory budget, not a throughput knob. Size it to absorb expected bursts and consumer stalls — roughly service rate times the stall you must ride out — and no larger, because worst-case queuing delay is capacity divided by service rate. Unbounded means no backpressure: memory grows until failure and every item is stale by the time it is served.

open as a page

Your team is deciding whether to build a stateful system on the actor model rather than stateless services over a shared database. What do actors give you, where does the model break down, and how would you decide?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Actors fit entity-shaped, stateful, high-touch domains: isolated state, serialized per-entity access, supervision, natural sharding. They break down on cross-entity transactions, hot keys (one actor equals one core), lost stack traces and harder debugging, weak delivery guarantees, and state that must survive restarts. Decide by whether per-entity ownership is the dominant access pattern.

open as a page