Reputation: 12366
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
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
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:
@
is a reader macro for the deref
fn, so (fn [p] @p)
is equivalent to just deref
.dosync
or not at all. For map
pings 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