« Back to Index

Reading “Programming Concurrency on the JVM” I found an example (which I’ve modified below) using Clojure to solve a classic concurrency dilemma by using the STM to help keep things sane.

View original Gist on GitHub

stm_skew.clj

; we need to have 1000 (or more) across both accounts
; if we have less than a 1000 then there is a violation

(def current-account (ref 500))
(def savings-account (ref 600))

; the `alter` function allows us to modify a reference (`@from` in this case
; which points to either the reference `current-account` or `savings-account`)

(defn unsafe-withdraw [from constraint amount]
  (dosync
    (let [total (+ @from @constraint)]
      (Thread/sleep 1000) ; blocks & so allows context switch to other future(thread) to start
      (if (>= (- total amount) 1000)
        (alter from - amount)
        (println "Sorry, can't withdraw due to constraint violation")))))

(defn safe-withdraw [from constraint amount]
  (dosync
    (let [total (+ @from (ensure constraint))] ; the `ensure` function is the secret sauce!
                                               ; also notice we don't deference the value (@constraint)
      (Thread/sleep 1000)
      (if (>= (- total amount) 1000)
        (alter from - amount)
        (println "Sorry, can't withdraw due to constraint violation")))))

; `ensure` let's us tell a transaction (remember `dosync` is using the STM)
; to watch a variable that is read and not modified.
; STM will ensure writes are comitted only if the values we've read have not
; changed outside the transaction. It'll then retry the transaction otherwise.

; UNSAFE EXAMPLE EXECUTION

(println "UNSAFE: BEFORE")
(println "Current Account balance is" @current-account)
(println "Savings Account balance is" @savings-account)
(println "Total balance is" (+ @current-account @savings-account))

(future (unsafe-withdraw current-account savings-account 100))
(future (unsafe-withdraw savings-account current-account 100))

(Thread/sleep 2000) ; allowing enough time for both futures(threads) to complete

(println "UNSAFE: AFTER")
(println "Current Account balance is" @current-account)
(println "Savings Account balance is" @savings-account)
(println "Total balance is" (+ @current-account @savings-account))

; OUTPUT (notice our code is not thread-safe as we've violated the account requirements)...

; UNSAFE: BEFORE
; Current Account balance is 500
; Savings Account balance is 600
; Total balance is 1100

; UNSAFE: AFTER
; Current Account balance is 400
; Savings Account balance is 500
; Total balance is 900

; ...WE'RE UNDER A 1000 ACROSS BOTH ACCOUNTS :-(


; SAFE EXAMPLE EXECUTION

(dosync (ref-set current-account 500)) ; reset value
(dosync (ref-set savings-account 600)) ; reset value

(println "SAFE: BEFORE")
(println "Current Account balance is" @current-account)
(println "Savings Account balance is" @savings-account)
(println "Total balance is" (+ @current-account @savings-account))

(future (safe-withdraw current-account savings-account 100))
(future (safe-withdraw savings-account current-account 100))

(Thread/sleep 2000) ; allowing enough time for both futures(threads) to complete

(println "SAFE: AFTER")
(println "Current Account balance is" @current-account)
(println "Savings Account balance is" @savings-account)
(println "Total balance is" (+ @current-account @savings-account))

; OUTPUT (notice our code is now thread-safe)...

; SAFE: BEFORE
; Current Account balance is 500
; Savings Account balance is 600
; Total balance is 1100

; SAFE: AFTER
; Current Account balance is 400
; Savings Account balance is 600
; Total balance is 1000

; ...ONLY ISSUE IS THAT IT SEEMS LIKE THE STM HASN'T RETRIED THE TRANSACTION?
; BECAUSE THERE IS NO WARNING MESSAGE PRINTED (AS PER THE `ELSE` STATEMENT)
; ALSO! MOST OF THE TIME THE BALANCE STAYS AS 1100 (NOTHING DEDUCTED FROM EITHER ACCOUNT)
; AND ON THE RARE OCCASION THE SAVINGS ACCOUNT WILL BE DEDUCTED FROM RATHER THAN THE CURRENT ACCOUNT