Konstantin Milyutin
Konstantin Milyutin

Reputation: 12366

Understanding STM properties in Clojure

I'm going through the book 7 concurrency models in 7 weeks. In it philosophers are represented as a number of ref's:

(def philosophers (into [] (repeatedly 5 #(ref :thinking))))

The state of each philosopher is flipped between :thinking and :eating using dosync transactions to ensure consistency.

Now I want to have a thread that outputs current status, so that I can be sure that the state is valid at all times:

(defn status-thread []
  (Thread.
    #(while true
      (dosync
        (println (map (fn [p] @p) philosophers))
        (Thread/sleep 100)))))

We use multiple @ to read values of each philosopher. It can happen that some refs are changed as we map over philosophers. Would it cause us to print inconsistent state although we don't have one?

I'm aware that Clojure uses MVCC to implement STM, but I'm not sure that I apply it correctly.

My transaction contains side effects and generally they should not appear inside a transaction. But in this case, transaction will always succeed and side effect should take place only once. Is it acceptable?

Upvotes: 3

Views: 280

Answers (2)

Arthur Ulfeldt
Arthur Ulfeldt

Reputation: 91554

This contingency was included in the STM design.

This problem is explicitly solved by combining agents with refs. refs guarantee that all messages set to agents in a transaction are sent exactly once and they are only sent when the transaction commits. If the transaction is retried then they will be dropped and not sent. When the transaction does eventually get through they will be sent at the moment the transaction commits.

(def watcher (agent nil))

(defn status-thread []
  (future
   (while true
     (dosync
      (send watcher (fn [_] (println (map (fn [p] @p) philosophers))))
      (Thread/sleep 100)))))

The STM guarantees that your transaction will not be committed if the refs you deref during the transaction where changes in an incompatible way while it was running. You don't need to explicitly worry about derefing multiple refs in a transaction (that what the STM was made for)

Upvotes: 0

Magos
Magos

Reputation: 3014

Your transaction doesn't really need a side effect, and if you scale the problem up enough I believe the transaction could fail for lack of history and retry the side effect if there's a lot of writing going on. I think the more appropriate way here would be to pull the dosync closer in. The transaction should be a pure, side-effect free fact finding mission. Once that has resulted in a value, you are then free to perform side effects with it without affecting the STM.

(defn status-thread []
  (-> #(while true
         (println (dosync (mapv deref philosophers)))
         (Thread/sleep 100))
    Thread.
    .start)) ;;Threw in starting of the thread for my own testing

A few things I want to mention here:

  1. @ is a reader macro for the deref fn, so (fn [p] @p) is equivalent to just deref.
  2. You should avoid laziness within transactions as some of the lazy values may be evaluated outside the context of the dosync or not at all. For mappings that means you can use e.g. doall, or like here just the eagerly evaluated mapv variant that makes a vector rather than a sequence.

Upvotes: 1

Related Questions