In order to deeply appreciate how Clojure removes the pain of lock-
based concurrency, I am re-implementing the example code from Java
Concurrency in Practice [http://www.javaconcurrencyinpractice.com/] in
Clojure. Consider Listing 2.8, which demonstrates creating fine-
grained locks around a simple cached calculation:
// elided for brevity
public class CachedFactorizer extends GenericServlet implements
Servlet {
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger[] lastFactors;
@GuardedBy("this") private long hits;
@GuardedBy("this") private long cacheHits;
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = null;
synchronized (this) {
++hits;
if (i.equals(lastNumber)) {
++cacheHits;
factors = lastFactors.clone();
}
}
if (factors == null) {
factors = factor(i);
synchronized (this) {
lastNumber = i;
lastFactors = factors.clone();
}
}
encodeIntoResponse(resp, factors);
}
}
And in Clojure (stripping out the Servlet noise):
(def *last-number* (ref nil))
(def *last-factors* (ref nil))
(def *count* (ref 0))
(def *cache-hits* (ref 0))
(defn cached-factor [n]
(dosync (commute *count* inc))
(or (dosync (if (= n @*last-number*)
(do (commute *cache-hits* inc) @*last-factors*)))
(let [factors (factor n)]
(dosync
(do (ref-set *last-number* n)
(ref-set *last-factors* factors))))))
A few questions/comments:
(1) Are the ref-sets overly strict? Could commute be used instead? I
am happy with last-in wins, so long as *last-number* and *last-
factors* change in tandem.
(2) In the Java example there are separate synchronized blocks to
increase possible concurrency. I have tried to write the Clojure
transactions similarly, in particular making sure the "expensive"
calculation (factor) is outside a transaction. How can it be done
better?
(3) Bonus: How idiomatic is my Clojure?
Thanks! I am truly enjoying Clojure.
Stuart
(def hits (ref 0))
(def cache (ref {:n nil :factors nil}))
(def cache-hits (ref 0))(dosync (commute hits inc))
(defn cached-factor [n]
(let [cached @cache]
(if (= n (cached :n))
(dosync (commute cache-hits inc)
(cached :factors))
(let [factors (factor n)](commute cache (fn [_] {:n n :factors factors})))
(dosync
factors))))
The ref-sets were needed in your example, but with a composite cache,
you can use the commute trick above (commute with a fn that ignores
its arg) to get last-one-in-wins, since this is just a last-value-only
cache.