Clojure
2026-08-20 10:15
Introducing Clojure claimed Clojure separates values from identities,
that immutability makes shared mutable state largely moot,
and named vars, atoms, refs and agents.
Then it stopped.
Almost nothing here is a concurrency primitive.
No mutexes. No critical sections.
The work is done by making values immutable —
and defining the one moment an identity takes a new one.
The real world is full of perceived changes: people change, plans change, the weather changes, and the balance in a bank account changes.
The problem is sharing it between threads and updating it.
Lost updates — two threads read 10, both write 11. Answer should be 12.
Dirty reads — reading data mid-update.
Unrepeatable reads — never able to read the same data twice.
Phantom reads — reading data that has been deleted.
Locks. Only one thread runs a protected section at a time.
Reasonable — until more than one piece of data needs a coordinated change.
No compile-time warning. No runtime warning.
Just a program that behaves unexpectedly.
The knowledge of what to lock cannot be expressed in the program.
Everyone in the software industry knows how well documentation works.
| Issue | Description |
|---|---|
| Deadlock | Threads each wait for locks the other holds. |
| Starvation | A thread never gets the resources to finish. |
| Livelock | Both keep moving, neither progresses. Two people in a hallway. |
| Race condition | Interleaving produces the wrong result — rarely, and unrepeatably. |
these languages conflate the idea of what Rich Hickey calls identity with that of state.
As a child: Disney and Pixar.
As a grownup: Tim Burton and Robert Zemeckis.
favorite-movies changed over time.
Or did it?
There are two sets. Neither ever changed.
What changed is which set the entity refers to.
Which gives a definition:
State is the value of an identity at a particular point in time.
The identity never changes; it refers to different values over time.
Extend that from numbers and strings to everything.
If a value cannot change:
All that is left is the instant an identity swaps one value for another.
Copying on every update grows linearly with size.
Unusable in production.
An immutable structure must:
A persistent data structure is one that preserves the previous version of itself when it’s modified.
All of Clojure’s core structures are persistent.
They share structure instead of copying.
| Type | Useful for |
|---|---|
| ref | Shared, synchronous, coordinated changes |
| agent | Shared, asynchronous, independent changes |
| atom | Shared, synchronous, independent changes |
| var | Isolated changes — thread-local |
Three axes: coordinated or independent · synchronous or asynchronous · shared or thread-local
Reading needs no transaction and never blocks.
And commute for commutative operations — more concurrency, weaker ordering.
Database transactions, applied to shared memory.
Lock-free. And optimistic, where locking is pessimistic.
Any number of threads may begin the transaction.
Changes are isolated — only the changing thread sees them.
The first to finish commits. Others abort and retry.
No durability — this is volatile memory, not a database.
Each thread gets a snapshot when its transaction starts.
readers never block writers (or other readers) … writers never block readers either
A transaction can be retried.
So the code inside a dosync must be free of side effects.
+ is applied to the agent’s current value and 700, later, on another thread.
The send call returns immediately.
send — fixed pool, for CPU-bound actions
send-off — expanding pool, for actions that block
A blocking action sent with send occupies a thread others need.
Mostly a testing tool.
Further sends fail too, until cleared.
An agent that has silently stopped accepting work is a real failure mode.
A dosync may retry, so it must be pure.
But real programs must send an email when a transaction commits.
Sends made inside a transaction are held until it commits —
and discarded if it retries or aborts.
So an agent is the correct place for the side effect.
Exactly once, only on success.
Synchronous, unlike agents.
Independent, unlike refs.
Read the current value · compute the new one · install it only if unchanged.
If it changed — discard and retry.
So the function passed to swap! must be free of side effects.
That is the same rule as the STM’s, by a different mechanism.
It follows from optimistic concurrency generally.
Two atoms changed one after another are two atomic changes.
With a moment in between where one has moved and the other has not.
If that matters, you needed refs.
Each thread rebinds for itself. Nothing to coordinate.
Identical for all four.
Reading is never coordinated, never blocks, cannot fail.
| Refs | Agents | Atoms |
|---|---|---|
(ref-set ref v) |
(send agent f & args) |
(reset! atom v) |
(alter ref f & args) |
(send-off agent f & args) |
(swap! atom f & args) |
(commute ref f & args) |
(compare-and-set! atom old new) |
Same shape: a function of the old value, installed under defined rules.
The same for all four.
Four genuinely different semantics behind one interface.
The variation is confined to the one place it must be —
the moment of change.
Only useful for isolating changes.
Not coordinating. Not sharing.
Cannot be written to by multiple parts of your code.
The vast majority of the time you’ll be using atoms because you don’t need anything more.
Two drawbacks: no coordination, and no side effects in swap!.
Split state into several refs and update them atomically in dosync.
Same tradeoff as one big lock versus several small ones.
Changes still must be side-effect free.
The only type that tolerates side effects.
The cost: error states to check and clear, and asynchronous completion.
A mostly pure mutation with refs —
plus a little side effect with an agent,
which runs only if the transaction succeeds.
Changing two atoms and worrying about the gap between them?
You have discovered your change was coordinated after all.
unlike reference types they can only ever have one value
What distinguishes them: the value may not yet be known.
Fifteen seconds.
About five seconds on four cores.
The longest, not the sum.
A future computes its own value — you decide what work happens.
A promise is filled by someone else — you decide when to wait.
A future is a computation. A promise is a rendezvous.
No mutexes. No critical sections.
Making values immutable removes almost all of the problem.
What remains is the single moment an identity takes a new value.
Evolving Clojure Through Macros changes subject entirely.
Back to a thread running since unit 1:
your program is a list, and the compiler reads it as data.
Clojure · State and the Concurrent World