Skip to content
AgileLogics
6 min read

Refs and Agents

Prerequisites: Atoms

Atoms manage one independent value. Clojure has two more reference types for other situations.

Type Coordinated? Synchronous? Use for
Atom no yes independent state (most cases)
Ref yes yes several values that must change together
Agent no no background, sequential updates

Refs and transactions

(def checking (ref 100))
(def savings (ref 50))

(defn transfer! [from to amount]
  (dosync
    (when (< @from amount)
      (throw (ex-info "Insufficient funds" {:available @from})))
    (alter from - amount)
    (alter to + amount)))

(transfer! checking savings 30)
(println @checking @savings)

(try
  (transfer! checking savings 500)
  (catch Exception e (println (ex-message e) (ex-data e))))
(println @checking @savings (+ @checking @savings))
70 80
Insufficient funds {:available 70}
70 80 150
  • dosync runs a transaction using Clojure's software transactional memory (STM). All changes inside happen together or not at all — if an exception is thrown, nothing changes.
  • alter applies a function to a ref inside a transaction; ref-set sets it; commute is for order-independent updates like counters.
  • If two transactions conflict, one is retried automatically, so keep side effects (printing, I/O) out of dosync.

Many concurrent transfers

(def accounts (vec (repeatedly 5 #(ref 1000))))

(let [jobs (doall
             (for [i (range 1000)]
               (future
                 (dosync
                   (let [from (accounts (mod i 5))
                         to (accounts (mod (* 3 i) 5))]
                     (alter from - 1)
                     (alter to + 1))))))]
  (run! deref jobs))

(println (map deref accounts))
(println "total:" (reduce + (map deref accounts)))
(shutdown-agents)
(1000 1000 1000 1000 1000)
total: 5000

A thousand concurrent transactions, no locks, and the total is always exactly 5,000.

Agents

(def log (agent []))

(dotimes [i 5]
  (send log conj (str "event " i)))

(await log)                       ; wait for queued actions
(println @log)

(def failing (agent 0))
(send failing / 0)
(Thread/sleep 100)
(println (type (agent-error failing)))
(shutdown-agents)
[event 0 event 1 event 2 event 3 event 4]
java.lang.ArithmeticException
  • send queues a function to run on the agent's value in the background; actions for one agent run one at a time, in order.
  • send-off is the same but for blocking work like I/O.
  • A failed action puts the agent in an error state (agent-error); use restart-agent or an :error-handler to recover.
  • Call (shutdown-agents) at the end of a program that used agents or futures, or the JVM waits about a minute for idle threads before exiting.

In practice, atoms cover most needs; refs are rare, and agents are mostly replaced by future, executors, or core.async (Level 3).