3
votes

I have some non-thread-safe code (a writer to shared data) that can only be called from multiple threads in a serialised manner, but I don't want to block any other thread-safe work (multiple readers) when this code is not being called.

This is essentially a multiple reader / single writer type locking situation where writers need to exclude both readers and other writers.

i.e. I have two functions:

(defn reader-function [] ....) // only reads from shared data

(defn writer-function [] ....) // writes to shared data

And a number of threads that are running (possibly in a loop) the following:

(do 
  (reader-function)
  ...
  (writer-function))

If any single thread is executing the writer function, all the other threads must block. i.e. at any one time either:

  • one thread is executing the writer and all others are blocked
  • multiple threads are executing the reader function, possibly some threads are blocked waiting to execute the writer once all readers are completed

What's the best way to achieve this kind of synchronisation in Clojure?

2

2 Answers

3
votes

Put your data in a ref. The data should be a Clojure data structure (not a Java class). Use dosync to create a transaction around the read and write.

Example. Because you split your writer into a separate function, that function must modify a ref with something like an alter. Doing so requires a transaction (dosync). You could rely on writer being called only in a dosync but you can also put a dosync inside the write and rely on nested transactions doing what you want - this makes writer safe to call either in or out of a transaction.

(defn reader [shared] 
  (println "I see" @shared))

(defn writer [shared item]
  (dosync 
    (println "Writing to shared")
    (alter shared conj item)))

;; combine the read and the write in a transaction
(defn combine [shared item]
  (dosync 
    (reader shared)
    (writer shared item)))

;; run a loop that adds n thread-specific items to the ref
(defn test-loop [shared n]
  (doseq [i (range n)]
    (combine shared (str (System/identityHashCode (Thread/currentThread)) "-" i))
    (Thread/sleep 50)))

;; run t threads adding n items in parallel
(defn test-threaded [t n]
  (let [shared (ref [])]
    (doseq [_ (range t)]
      (future (test-loop shared n)))))

Run the test with something like (test-threaded 3 10).

More info here: http://clojure.org/refs

You didn't ask about this case, but it's important to note that anyone can read the shared ref by derefing it at any time. This does not block concurrent writers.

0
votes

Take a look at java.util.concurrent.locks.ReentrantReadWriteLock. This class allow you to have multiple readers that do not contend with each other on one writer at a time.