~ / track F / concurrency models

CSP vs the actor model

Advanced

CSP (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

AspectCSPActors
What's namedChannelRecipient (actor)
Buffer lives onChannelEach actor's mailbox
Sender knowsChannelSpecific actor
DecouplingSender ⊥ receiverSender knows receiver
BackpressureBuilt-in (channel full → block)Mailbox grows unbounded by default
Failure modelChannels closeActors 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:

  1. CSP: how many channels do you need, and who owns them?
  2. 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.)

 status: new