~ / track B / clojure advanced
Software Transactional Memory
AdvancedSTM lets you treat a group of memory operations the way a database treats a transaction: they all commit, or none of them do. You group reads and writes inside a transaction; the runtime detects conflicts with concurrent transactions and retries until your operation lands consistently. No locks, no lock ordering, no deadlocks — you describe the invariant, the system maintains it.
The core API in Clojure
We covered refs in the state types topic; here we look at STM from the concept side. Three operations:
dosync— open a transaction.ref-set— set the value of a ref.alter/commute— apply a pure function to update a ref.
If two threads run the transfer concurrently and both read a = 100, only
one can commit. The other re-runs from the start, this time reading the
already-updated value. Your code never sees an inconsistent snapshot.
Why retry-based, not lock-based
Locks force you to prevent concurrent access; STM lets concurrent access happen and detects conflicts. The result:
- No deadlocks. There's no resource to acquire in a wrong order.
- Composable. Nest two
dosyncblocks; you get one bigger transaction, not two transactions that might interleave. - Optimistic. When conflicts are rare, you pay almost no overhead. When
they're frequent, you pay retries (and there's a knob to switch some
updates to
commute, which doesn't conflict when the operations are commutative).
The catch: pure transaction bodies
Because the transaction body may run more than once (on retry), it must be a pure function of the current refs' values:
The same rule as swap! on an atom, scaled to the multi-ref case: side
effects belong after the transaction commits, with the values it returns.
commute: when order doesn't matter
commute is alter's commutative cousin. If your update function is
commutative with respect to itself (e.g. inc, +, merge of disjoint
keys), commute lets multiple transactions update the ref without
conflicting — each transaction's contribution is folded in at commit time.
In a multi-threaded setting, commute allows substantially more throughput
than alter on the same ref.
When STM is the right tool
- A small group of values must update together (the canonical bank transfer / inventory adjustment).
- You want composability — nested transactions should be one transaction.
- Conflicts are rare, so retries are cheap.
For independent, single-value updates, an atom is dramatically simpler
and just as safe. STM is a power tool; reach for it when atoms can't express
the invariant.
Check yourself
? quiz
Why must the body of `dosync` be pure?
Exercise
Imagine a small bank with three accounts. Write a transfer-all that, in one
transaction, moves an equal share of account-a's balance to account-b
and account-c. The original account-a should reach 0; the others should
each gain half (assume the balance is even). Verify that calling
transfer-all from many threads concurrently still leaves the total balance
invariant.