mikera
mikera

Reputation: 106401

Synchronising threads for multiple readers / single writer in Clojure

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:

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

Upvotes: 3

Views: 641

Answers (2)

Alex Miller
Alex Miller

Reputation: 70239

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.

Upvotes: 3

Ralph
Ralph

Reputation: 32304

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.

Upvotes: 0

Related Questions