commute

added
1.0

ns
clojure.core

type
function

(commute ref fun & args)

Must be called in a transaction. Sets the in-transaction-value of
ref to:

(apply fun in-transaction-value-of-ref args)

and returns the in-transaction-value of ref.

At the commit point of the transaction, sets the value of ref to be:

(apply fun most-recently-committed-value-of-ref args)

Thus fun should be commutative, or, failing that, you must accept
last-one-in-wins behavior.  commute allows for more concurrency than
ref-set.

                user=> (def counter (ref 0))
#'user/counter

;; deciding whether to increment the counter takes the terribly long time
;; of 100 ms -- it is decided by committee.
user=> (defn commute-inc! [counter]
         (dosync (Thread/sleep 100) (commute counter inc)))
#'user/commute-inc!
user=> (defn alter-inc! [counter]
         (dosync (Thread/sleep 100) (alter counter inc)))
#'user/alter-inc!

;; what if n people try to hit the counter at once?
user=> (defn bombard-counter! [n f counter]
         (apply pcalls (repeat n #(f counter))))
#'user/bombard-counter!

;; first, use alter.  Everyone is trying to update the counter, and
;; stepping on each other's toes, so almost every transaction is getting 
;; retried lots of times:
user=> (dosync (ref-set counter 0))
0
user=> (time (doall (bombard-counter! 20 alter-inc! counter)))
"Elapsed time: 2007.049224 msecs"
(3 1 2 4 7 10 5 8 6 9 13 14 15 12 11 16 17 20 18 19)
;; note that it took about 2000 ms = (20 workers * 100 ms / update)

;; now, since it doesn't matter what order people update a counter in, we
;; use commute:
user=> (dosync (ref-set counter 0))
0
user=> (time (doall (bombard-counter! 20 commute-inc! counter)))
"Elapsed time: 401.748181 msecs"
(1 2 3 4 5 9 10 6 7 8 11 15 13 12 14 16 19 17 18 20)
;; notice that we got actual concurrency this time.
            
                ; Note that commute will ALWAYS run the update function TWICE. 
; Example courtesy of "Clojure for the Brave and True"
; https://github.com/flyingmachine/brave-clojure-web

(defn sleep-print-update
  [sleep-time thread-name update-fn]
  (fn [state]
    (Thread/sleep sleep-time)
    (println (str thread-name ": " state))
    (update-fn state)))

(def counter (ref 0))
(future (dosync (commute counter (sleep-print-update 100 "Commute Thread A" inc))))
(future (dosync (commute counter (sleep-print-update 150 "Commute Thread B" inc))))

; printed output is:
Commute Thread A: 0   ; (after 100ms)
Commute Thread B: 0   ; (after 150ms)
Commute Thread A: 0   ; (after 200ms)
Commute Thread B: 1   ; (after 300ms)