~ / track F / concurrency models
CSP vs the actor model
AdvancedCSP (Communicating Sequential Processes) and the actor model are two
mainstream answers to "how do concurrent components talk to each other
without sharing memory?" Both treat concurrency as message-passing, but they
differ on what you name and where the buffer is. Understanding the
difference clarifies why Clojure's core.async (CSP) and Erlang/Akka
(actors) feel so different in practice.
CSP: name the channel
In CSP (Hoare, 1978), processes communicate through named channels. Processes are anonymous — what matters is the channel. A producer doesn't know which consumer will read; a consumer doesn't know which producer wrote. Backpressure is natural: the writer blocks if the channel is full, the reader blocks if it's empty.
Clojure's core.async is CSP:
(let [c (chan)]
(go (>! c :hi))
(<!! c))
The chan is the named conduit; the go blocks are anonymous workers
attached to it. You can fan in by giving many writers the same channel, or
fan out by giving many readers the same channel.
Actors: name the recipient
In the actor model (Hewitt, 1973), every concurrent entity is an actor with an identity (its address / PID). You send messages to a specific actor by name. The actor has a private mailbox; messages queue up; the actor processes them one at a time. There are no shared channels — every mailbox is owned by exactly one actor.
Erlang's syntax illustrates:
Pid ! {hello, World}. %% send to actor named Pid
receive
{hello, X} -> ...
end.
Pid is the actor's identity. The mailbox is on the actor itself, not on a
shared object.
The crucial difference
| Aspect | CSP | Actors |
|---|---|---|
| What's named | Channel | Recipient (actor) |
| Buffer lives on | Channel | Each actor's mailbox |
| Sender knows | Channel | Specific actor |
| Decoupling | Sender ⊥ receiver | Sender knows receiver |
| Backpressure | Built-in (channel full → block) | Mailbox grows unbounded by default |
| Failure model | Channels close | Actors crash + supervision trees |
CSP makes routing/scaling decisions outside the processes — change the channel topology, the processes don't notice. Actors put identity on the actor, which makes one-to-one direct messaging more natural but also means you need to know who to talk to.
In practice
- Clojure / Go use CSP. Worker pools, pipelines, fan-in/fan-out shapes are particularly clean.
- Erlang / Elixir / Akka use actors. Long-lived stateful processes, supervision trees, and fault tolerance shine.
- Most modern systems borrow from both. You can build "actor-like" patterns on channels and vice-versa; the differences are conventions, not absolute prohibitions.
Shared ground
Both models eliminate shared mutable state at the language level. State lives inside a single process/actor; concurrency is achieved by sending messages instead of sharing data. That alone removes most of the bugs (race conditions, lock ordering, deadlocks-on-mutexes) that haunt thread-and-lock code.
Check yourself
? quiz
A producer pushes faster than the consumer can handle. What happens by default in each model?
Exercise
Sketch (in prose) how you'd build a request/response pattern between two services in each model:
- CSP: how many channels do you need, and who owns them?
- Actors: what does the message envelope have to include for the response to find its way back?
(Hint: for actors, the sender's PID often goes in the message itself.)