State and the Concurrent World

Keywords

ver. 1.0.0

How Clojure solves concurrency by separating immutable values from mutable identities and offering four managed reference types plus futures/promises.

This unit teaches a principled approach to concurrency in Clojure: why shared mutable state causes hard-to-reproduce anomalies, how to separate immutable values from mutable identities, and how to use Clojure’s four managed reference types (refs, agents, atoms, and vars) plus futures and promises. By the end you can pick the right reference for a problem, use transactions and MVCC with refs, manage asynchronous and synchronous changes with agents and atoms, use thread-local bindings with vars, and run parallel work with futures and promises.

The unit explains why conventional locking is an inadequate response to concurrency and presents a clearer model built on immutable values and mutable identities. It shows that values are timeless and safe to read, while identities name a succession of values over time; using persistent data structures keeps this efficient.

It introduces managed references — mutable containers for immutable values — and teaches the four kinds Clojure provides and how they differ: - Refs: coordinated, transactional updates performed inside dosync using ref-set, alter, or commute. Backed by STM and multiversion concurrency control, transactions provide atomicity, consistency and isolation and may retry. - Agents: asynchronous, independent change by sending functions to run later on a thread pool (send for CPU-bound, send-off for blocking). Agents are also the sanctioned way to perform side effects referenced from inside transactions and include error handling semantics. - Atoms: synchronous, uncoordinated, single-identity change via swap! and reset!, implemented as compare-and-set with a retry loop so update functions may run more than once. - Vars: thread-local bindings changed with binding, used for state that isn’t shared and needs no coordination.

All four share a common access model — a constructor, a uniform read (deref / @), a type-specific mutate operation, and the same watch mechanism (add-watch / remove-watch) — so learning one largely teaches the others.

The unit reduces the choice among reference types to two core questions: does the change need coordination with other changes? and must it be synchronous? This yields a simple decision map: coordinated → refs; uncoordinated synchronous → atoms; asynchronous → agents; not shared → vars.

Finally, the unit covers parallelism without shared state: futures run work on another thread with deref to obtain the result, and promises are single-assignment containers filled with deliver.

After completing the unit a learner will be able to: - Name the anomalies caused by shared mutable state and explain why locking is fragile. - Distinguish immutable values from mutable identities and reason about safety of reads. - Use refs and transactions and explain the STM guarantees and retry behavior. - Use agents for asynchronous updates and handle their errors. - Use atoms for synchronous, uncoordinated updates and understand possible re-execution of update functions. - Use vars and thread-local binding when state need not be shared. - Use the common deref/watch interface to observe changes. - Choose the appropriate reference type for a problem. - Use futures and promises when parallelism needs no shared state.

This foundation prepares the learner to write safer concurrent Clojure programs and to move on to higher-level language extension techniques (such as macros) with a solid understanding of state and concurrency primitives.

Materials

Source document

  • Clojure in Action (2nd Edition), A. Rathore and F. Avila, Manning, Dec. 2015 — Link — Page 158-188