One of the selling points of Clojure is its excellent support for multithreading. Among other features, it has a very intuitive yet powerful implementation of Software Transactional Memory. There’s a nice introduction on it in R. Mark Volkmann’s famous article.
One notable feature of STM is a ref
– shared binding which can only be modified inside transactions. Transactions are wrapped in (dosync)
and behave similarly to database transactions. Value of a ref
can be set in 3 ways: ref-set
(obvious setter), commute
and alter
.
One thing that wasn’t obvious to me after reading the article was the difference between alter
and commute
. They both take a ref
and a function, set the ref
to result of running the function on previous value of the ref
, and return the new value.
I decided to test its behavior with this simple experiment. Define an integer counter. Start 50 threads that increase this counter with alter
and print the new value. After all threads finish, print the counter. Then repeat with commute
instead of alter.
Here’s the sample with alter
:
(def my_num (ref 0)) (defn inc_alter [] (dosync (try (println "Alter: " (alter my_num inc)) (catch Throwable t (do (println "Caught " (.getClass t)) (throw t)))))) (defn test_alter [] (let [threads (for [x (range 0 50)] (Thread. #(inc_alter)))] (do (println "---- Alter ----") (doall (map #(.start %) threads)) (doall (map #(.join %) threads)) (println "---- After loop: ----") (inc_alter)))) (test_alter)
The output can look like:
---- Alter ---- Alter: 1 Alter: 2 Alter: 3 ... Alter: 47 Caught clojure.lang.LockingTransaction$RetryEx Alter: 48 Caught clojure.lang.LockingTransaction$RetryEx Alter: 49 Caught clojure.lang.LockingTransaction$RetryEx Caught clojure.lang.LockingTransaction$RetryEx Alter: 50 ---- After loop: ---- Alter: 51
Explanation: when alter
detects that the ref
has been changed in another concurrent transaction, the whole transaction is rolled back. Then it is automatically retried. That’s fairly intuitive and straightforward.
When we replace alter
with commute, it’s quite a different story. The output can look like:
---- Commute ---- Commute: 1 Commute: 2 Commute: 3 ... Commute: 43 Commute: 45 Commute: 45 Commute: 45 Commute: 45 ---- After loop: ---- Commute: 51
The modification is commutative. No transaction is ever rolled back. But it also means that inside the transaction a stale value can be used, what is the reason for getting “45” several times.
It doesn’t work exactly like I described earlier (get value, commute, set value, return). In fact, when commute
is called it instantly returns result of running the function on the ref
. At the very end of transaction it performs the calculation again, this time synchronously (like alter
) updating the ref
. That’s why eventually the counter value is 51 even though the last thread printed 45.
By the way, have a look at the sample code and think about how much it would take to implement it your Java or C#. Not only is it multithreaded, but also synchronized, transactional (with ACID or at least ACI), serializable with auto-retry, and so on. So much going on in just 20 lines.