Understanding Clojure STM: Atoms, Refs, and Agents

Software Transactional Memory in Clojure

Jeffry Tandiono
4 min readJan 12, 2020

Software Transactional Memory (STM) is a concurrency control technique
analogous to database transactions for controlling access to shared
memory in concurrent computing. It is an alternative to lock-based synchronization. —
Wikipedia

In a nutshell, STM is a way to facilitate saved sharing of data between threads, also a way to implement state mutable object.

The simplest way to implement STM is an atom.

Atom

Atom provides synchronize transactional access of a single value.

(def count (atom 0))

You can set a value to an atom by reset

(reset! count 1)
> 1

Access the atom by using deref or using the shorthand of deref (‘@’)

(deref count)
> 1
@count
> 1
(reset! count 0)
@count
> 0

You can set the atom in a loop to increment the value

(dotimes [_ 1000]
(future (reset! count (inc @count))))
@count
> 860

Let me explain the function above step by step

  • @count
    to get the current value of count
  • inc
    to increment the value by 1
  • reset! count
    to set the current value to the new value
  • dotimes 1000 future
    running the future 1000 times

“But, the result is wrong?”

Yeah, that’s because when some deref count and increment the value, 140 times a future wrongly assigns the value.

To fix this problem, we can change to using swap. Swap takes a function as an argument rather than a value like reset.

(dotimes [_ 1000]
(future (swap! count inc)))
@count
> 1000

Swap will synchronize the order of the function that comes in and can give the correct result.

Atom can also use a validator to check the current value is set with the correct type.

(def count (atom 0 :validator integer?))(reset! count "string")
> java.lang.IllegalStateException: Invalid reference state

The problem of Atom

Let’s try to create a transfer on a bank in a simple code

(def acc1 (atom 0 :validator #(>= % 0)))
(def acc2 (atom 0 :validator #(>= % 0)))
(defn transfer [from-acct to-acct amt]
(swap! to-acct + amt)
(swap! from-acct - amt))
(reset! acc1 1000)
> 1000
(reset! acc2 1000)
> 1000
(dotimes [_ 1000]
(future (transfer acc2 acc1 100)))

Right there we created the code, to give a brief overview of the code

  • (def acc1 (atom 0 :validator #(>= % 0)))
    set the atom with a validator that the value should be bigger or equal than 0.
  • (defn transfer [from-acct to-acct amt] (swap! to-acct + amt) (swap! from-acct — amt))
    create a function name transfer with 3 parameters
  • (dotimes [_ 1000] (future (transfer acc2 acc1 100)))
    transfer the amount of 100 for 1000 times from acc2 to acc1

We should expect acc1 with a value of 2000 and acc2 with a value of 0.

(deref acc1)
> 101000
(deref acc2)
> 0

Somehow the value is added to the acc1 without checking the value of acc2. The problem here exists because the validator is not checked yet when the acc1 is added. That means that we haven’t done this in a transactional way, both the swap function is being seen as different functions.

That comes to the second STM: refs.

Refs

We can use refs to do the transaction.

(def acc1 (ref 1000 :validator #(>= % 0)))
(def acc2 (ref 1000 :validator #(>= % 0)))
(defn transfer [from-acct to-acct amt]
(dosync
(alter to-acct + amt)
(alter from-acct - amt)))
(dotimes [_ 1000]
(future (transfer acc2 acc1 100)))

Ref also accepts the validator with changes of swap to alter. But refs need to be inside a transaction by doing dosync.

(deref acc1)
> 2000
(deref acc2)
> 0

Nice, it works now. This happens because when the alter to reduce from-acct happens and fails, the increment to to-acct also fails.

We can also change alter to commute. The different from both of this is commute can happen parallel within a transaction (don’t have to happen in order).

The completely different from refs is agents.

Agents

Agent is for asynchronous update to a single value while ref is for synchronous update to multiple values.

Let’s see at what agent code looks like

(def my-agent (agent 0 :validator #(>= % 0)))(send my-agent inc)
> 1
(send my-agent inc)
> 2
(send my-agent inc)
> 2
(deref my-agent)
> 3

Send in agent is the same as reset on Atom and alter on Ref. But there is a wrong result on the third send. Well, that happens because agent run asynchronously, sometimes you can get the correct result, sometimes don’t.

But what happens if I run it with agent through the validator.

(def my-agent (agent 0 :validator #(>= % 0)))(send my-agent dec)
> java.lang.RuntimeException: Agent is failed, needs restart

It will raise an exception, and asking for restart. So to restart the agent

(restart-agent my-agent 0)

And now we can use the agent again. But you can remove the restart needed by adding error-mode options on declaration.

(def my-agent (agent 0 :validator #(>= % 0) :error-mode :continue))

Now you don’t need to restart the again, everytime the error raise, it will continue to works without restarting.

TLDR

The differences between Atom, Ref and Agent:

  1. Atom
    Atom share access to mutable state for every threads with change occurs synchronously.
  2. Ref
    Ref works similar to database transactions. Ref can be used for many operations in a safe transaction.
  3. Agent
    Agent share access to mutable state for every threads with change occurs asynchronously.

--

--

Jeffry Tandiono

Personal Growth Enthusiast | Software Developer | Love sharing | Check my stuff out!