2
\$\begingroup\$

I'm learning clojure ref types and was trying to make a robust counter generating function. Is there any error or improvement on the code? is there a simpler way? (I want the function to be thread-safe)

(defn get-counter []
  (let [a (agent 0)]
    (fn []
      (let [p (promise)]
        (send a (fn [old] 
                  (deliver p old)
                  (inc old)))
        @p))))

In some situations it works better with a future but I'm not sure why it is.

(defn get-counter []
  (let [a (agent 0)]
    (fn []
      (let [p (promise)]
        @(future 
           (send a (fn [old] 
                     (deliver p old)
                     (inc old)))
           @p)))))

EDIT - What I wanted the counter for. I was testing how the transactions worked and wanted a way of seeing the transaction retries. I wanted a function that returns numbers in sequence and never repeats a number or skips one.

(def counter (get-counter))

(def r (ref []))
(def a (atom []))

(repeatedly 2 #(future (dosync (let [c (counter)]
                                 (future (dosync (Thread/sleep 50)
                                               (let [c2 (counter)]
                                                 (swap! a conj [:foo c c2])
                                                 (alter r conj [:foo c c2]))))

                                 (swap! a conj [:bar c])
                                 (alter r conj [:bar c])))))

(deref r)
=> [[:bar 0] [:bar 2] [:foo 1 6] [:foo 0 9] [:foo 2 11]]
(deref a)
=> [[:bar 0] [:bar 1] [:bar 2] [:foo 1 3] [:foo 0 4] [:foo 2 5] [:foo 1 6] [:foo 0 7] [:foo 2 8] [:foo 0 9] [:foo 2 10] [:foo 2 11]]

From Elogent's answer I'm guessing I could just do something like:

(defn get-counter []
  (let [a (atom 0)]
    (fn []
      (swap! a inc))))

2nd EDIT - The only problem I could see in this solution is that if two calls happen very close to each other, the first call might get the greater value if it needs to retry.

Since another thread may have changed the value in the intervening time, it may have to retry, and does so in a spin loop. http://clojure.org/reference/atoms

\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

It seems like you're doing a lot of work to turn an agent, something which is inherently asynchronous, into something synchronous. Actually, Clojure has a synchronous state construct built in: the atom.

I'm not going to comment on the correctness of your implementation of get-counter, because a huge part of the design of the Clojure language is the fact that concurrency is hard, and it's better to get it right once than to have every programmer reimplement the same concurrency constructs themselves.

Instead, I'll give some examples of how easy it is to use an atom for a synchronized counter in Clojure. First, you'll need an atom, which you can create with whatever initial value you like:

(def counter (atom 0))

At any time, any thread can instantly and safely read the value of the counter using the deref function:

@counter
;=> 0

You can update the value of an atom using swap!, which takes a function and (optionally) some arguments. For our counter, we'll probably just use inc:

(swap! counter inc)
;=> 1

Since atoms are synchronous, swap! doesn't return until the value of the atom has been successfully updated.

But you don't need to stop at inc; you can pass any function you'd like to swap!, like inc' in case you're going to be counting for a long, long time:

(swap! counter inc')
;=> 2

Or you could add some arbitrary amount to the current value using + or +':

(swap! counter + 42)
;=> 44

Atoms are completely thread safe, so you can do something like this:

(dotimes [_ 1000]
  (future
    (dotimes [_ 1000]
      (swap! counter inc))))

And after all the swap!s complete, you're guaranteed to have all of your updates registered:

@counter
;=> 1000044
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.