3

I've read this SO question and http://clojure.org/refs, but I am still confused about how exactly ref-set works. (To some extent the two documents kind of lead me to believe two different things...)

Suppose that I have a transaction in Clojure that looks like this:

(def flag (ref false))
(dosync
    (long-computation-that-does-not-read-or-write-flag)
    (ref-set flag true))

Suppose that in the middle of the long computation, somebody else modifies flag. Will that cause my transaction to retry when I try to ref-set flag?

I could imagine the answer might be yes, since clojure.org says transactions guarantee that "No changes will have been made by any other transactions to any Refs that have been ref-set/altered/ensured by this transaction".

But I could also imagine the answer to be no, since I never read flag, and the clojure.org page suggests that "All *reads* of Refs will see a consistent snapshot of the 'Ref world' as of the starting point of the transaction". This is also what the linked SO answer would lead me to believe.

And a followup: supposing that instead of (ref-set flag true), I had done one of these:

  1. (alter flag (fn [_] true))
  2. (let [ignored @flag] (ref-set flag true))

I assume that both of those would constitute a read of flag, and so the transaction would have to retry?

Community
  • 1
  • 1
Ord
  • 5,693
  • 5
  • 28
  • 42

1 Answers1

3

Calling ref-set means that you have included flag in the tracked references for this transaction. Thus, a concurrent write to flag in some other transaction will cause a conflict and a retry.

Both of the followups modify flag (via alter and ref-set) and thus have the same result. The important thing here is not the read of flag, it's the write. If a transaction contains a read of a ref without a write, the transaction can succeed even if the read ref changes in a concurrent transaction. However, ensure can be used to include a read in the tracked references for a transaction (thus causing concurrent changes to fail).

Alex Miller
  • 69,183
  • 25
  • 122
  • 167
  • Okay, that makes sense. I suppose then that if the behaviour I want is: "I don't care what happens to flag during my transaction, so long as it is set to true at the end of the transaction", then I would use something like `(commute flag (fn [_] true))`. Is that right? – Ord Dec 15 '14 at 04:37
  • commute is an optimization to alter that allows transactions to be done out of order for performance. For example, if you have a counter, then updating it from two transactions could occur in either order with the same end result. In this case, I don't think that's what you want. You want ref-set. – Alex Miller Dec 15 '14 at 15:03
  • Is there really no way of saying "when this transaction commits, flag must be true" without creating an artificial dependence on the previous value of flag? What if the operation that precedes the flag set is really long and costly, and I'd really like to avoid redoing the entire transaction just because someone changed flag? – Ord Dec 17 '14 at 02:55
  • ref-set does not create a dependency on the previous value of the flag. It creates a constraint for coordinated change between transactions. If you don't want coordination, then maybe don't use a ref? You could store the flag in an atom and send the flag change to an agent - that will only happen when the transaction has completed. – Alex Miller Dec 17 '14 at 03:12
  • Okay, suppose I have a cache of some data source. It's expensive to rebuild the cache, so I will just live with an out-of-date cache in some instances. However, I need to have a flag which says whether the cache is up to date or not so that consumers that require absolutely-up-to-date info can bypass the cache if required. I have a thread that rebuilds the entire cache. When it finishes, it must update the flag. The flag-setting and cache-updating must happen together. But I don't care if someone else changes the flag during my update process. Is there a way of handling the flag in clojure? – Ord Dec 17 '14 at 03:43
  • Also, I don't really understand why commute wouldn't work. My impression was that the only situation that commute allows which alter/ref-set do not is when a transaction is ready to commit and the commuted ref has been changed since the transaction read point. Commute will allow just the commute portion of the transaction to be retried, whereas alter or ref-set would require the whole transaction be retried. Isn't that the behaviour I want in this case? – Ord Dec 17 '14 at 03:51