Clojure concurrency - part 2
May 17, 2010
Let's find out what happens when two Clojure threads try to simultaneously update a shared global variable.
(def N 0) (defn foo  (dotimes [i 100000] (def N (inc N)))) (defn start-threads  (let [t1 (Thread. foo) t2 (Thread. foo)] (do (.start t1) (.start t2) (.join t1) (.join t2)))) (defn main  (start-threads) N) (println (main))
Run the code many times. You will note that the value of `N' after both the threads have finished running (we wait for both threads to finish by calling `join') will not be 200000 always. You will get many seemingly random values less than 200000. Why is this so?
Let's say the current value of N is 100. After one iteration each of both the threads, the value of N should be 102.
Let's think of a scenario like this: the first thread has read the value of N and incremented it (to 101), but before it is assigned back to N, the second thread reads the value of N (it is still 100), increments it and assigns the incremented value back to N (so, N gets the value 101). Now, the first thread assigns the value it had earlier computed (ie, 101) back to N. The final value of `N' after one iteration of both the threads is now 101 and NOT 102. We have lost one increment!
It is now not very hard to see why we are getting values less than 200000 after both threads have finished running.
The easiest way to solve this problem is by using what are called Clojure `atoms'.
Here is how we create an atom in Clojure:
(def p (atom 0))
We have created `p', an `atom' with an initial value of 0. The value of the atom can be accessed like this:
(+ @p 1) ; prints 1
The most interesting operation on an `atom' is an atomic read-modify-write:
(swap! p inc)
The first argument of `swap!' is an atom and the second argument is a function. The function gets applied to the current value of the atom and the result is assigned back to the atom. This operation is logically `atomic' - errors like what we had seen in the first program will never occur.
We can rewrite our first program using atoms and it is always guaranteed to yield the correct value of 200000 for N.
(def N (atom 0)) (defn foo  (dotimes [i 100000] (swap! N inc))) (defn start-threads  (let [t1 (Thread. foo) t2 (Thread. foo)] (do (.start t1) (.start t2) (.join t1) (.join t2)))) (defn main  (start-threads) @N)
Test it on your machine!