~ / 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:
- dosyncclojure.core/dosyncRun a transaction; ref updates inside are coordinated and retry on conflict.view on clojuredocs → — open a transaction.
ref-set— set the value of a ref.- alterclojure.core/alterIn-transaction update of a ref: (alter r f args*).view on clojuredocs → / commuteclojure.core/commuteCommutative update of a ref; never causes a retry.view on clojuredocs → — apply a pure function to update a ref.
(def a (ref 100))
(def b (ref 0))
(dosync
(alter a - 25)
(alter b + 25))
[@a @b]
;; => [75 25]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 dosyncclojure.core/dosyncRun a transaction; ref updates inside are coordinated and retry on conflict.view on clojuredocs → blocks; 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 commuteclojure.core/commuteCommutative update of a ref; never causes a retry.view on clojuredocs →, 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:
;; BAD — println runs once per retry, possibly many times
(def n (ref 0))
(dosync
(println "running")
(alter n inc))The same rule as swap!clojure.core/swap!Atomically apply f to the atom's value.view on clojuredocs → on an atom, scaled to the multi-ref case: side effects belong after the transaction commits, with the values it returns.
commuteclojure.core/commuteCommutative update of a ref; never causes a retry.view on clojuredocs →: when order doesn't matter
commuteclojure.core/commuteCommutative update of a ref; never causes a retry.view on clojuredocs → is alterclojure.core/alterIn-transaction update of a ref: (alter r f args*).view on clojuredocs →'s commutative cousin. If your update function is
commutative with respect to itself (e.g. incclojure.core/incAdd one.view on clojuredocs →, +, mergeclojure.core/mergeRight-most map wins; nil-safe.view on clojuredocs → of disjoint
keys), commuteclojure.core/commuteCommutative update of a ref; never causes a retry.view on clojuredocs → lets multiple transactions update the ref without
conflicting — each transaction's contribution is folded in at commit time.
(def counter (ref 0))
(dosync (commute counter inc))
(dosync (commute counter inc))
@counter
;; => 2(refclojure.core/refCoordinated, synchronous reference; updated only inside dosync.view on clojuredocs →, dosyncclojure.core/dosyncRun a transaction; ref updates inside are coordinated and retry on conflict.view on clojuredocs →, alterclojure.core/alterIn-transaction update of a ref: (alter r f args*).view on clojuredocs →, and commuteclojure.core/commuteCommutative update of a ref; never causes a retry.view on clojuredocs → are JVM-only — the in-browser REPL doesn't ship Clojure's STM, so the snippets in this lesson are read-only.)
In a multi-threaded setting, commuteclojure.core/commuteCommutative update of a ref; never causes a retry.view on clojuredocs → allows substantially more throughput than alterclojure.core/alterIn-transaction update of a ref: (alter r f args*).view on clojuredocs → 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 atomclojure.core/atomMutable, synchronous, uncoordinated reference type.view on clojuredocs → 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.