user3243135
user3243135

Reputation: 820

What is happening when I def a variable from multiple threads in Clojure?

When I do something like:

(def x 123)
(future (def x 456))

The def in the second thread ends up modifying the value in the main thread. I understand this is not idiomatic and that I should be using atoms or something more complex. That aside, however, this is contrary to my expectation because I've read in various places that vars are "dynamic" or "thread-local".

So, what exactly is happening here? Is the second thread making an unsafe assignment, akin to what would happen if you did the equivalent in C? If so, does clojure "allow" for the possibility of other unsafe operations like appending to a list from multiple threads and ending up with an inconsistent data structure?

Upvotes: 2

Views: 350

Answers (2)

Michiel Borkent
Michiel Borkent

Reputation: 34800

Thread safe modification of the root binding of vars can be done with alter-var-root:

(do (future (Thread/sleep 10000) (alter-var-root #'v inc)) (def v 2))

Calling def multiple times with the same name just overwrites the root binding where the last call wins.

However, in idiomatic Clojure def is to be used only at the top level (exceptions like macros aside).

Upvotes: 1

Taylor Wood
Taylor Wood

Reputation: 16194

First, def is a special form in Clojure, worth reading up about.

I've read in various places that vars are "dynamic" or "thread-local".

They can be but this isn't the typical usage. From the guide:

def always applies to the root binding, even if the var is thread-bound at the point where def is called.

To demonstrate this:

(def ^:dynamic foo 1)
(binding [foo 2] ;; thread-local binding for foo
  (prn foo)      ;; "2"
  (def foo 3)    ;; re-defs global foo var
  (prn foo))     ;; "2" (still thread-local binding value)
(prn foo)        ;; "3" (now refers to replaced global var)

And with multiple threads:

(def ^:dynamic foo 1)
(future
  (Thread/sleep 500)
  (prn "from other thread" foo))
(binding [foo 2]
  (prn "bound, pre-def" foo)
  (def foo 3)
  (Thread/sleep 1000)
  (prn "bound, post-def" foo))
(prn "finally" foo)
;; "bound, pre-def" 2
;; "from other thread" 3
;; "bound, post-def" 2
;; "finally" 3

So, what exactly is happening here? Is the second thread making an unsafe assignment, akin to what would happen if you did the equivalent in C?

Depends on your definition of unsafe, but it's certainly uncoordinated and non-atomic with regard to multiple threads. You could use alter-var-root to change the var atomically, or use something like a ref or atom for mutable state.

If so, does clojure "allow" for the possibility of other unsafe operations like appending to a list from multiple threads and ending up with an inconsistent data structure?

Not with its persistent data structures, which are conceptually copy-on-write (though the copies share common knowledge for efficiency). This confers a lot of benefits when writing multi-threaded code in Clojure and other functional languages. When you append to a (persistent) list data structure, you're not modifying the structure in-place; you get back a new "copy" of the structure with your change. How you handle that new value, presumably by sticking it in some global "bucket" like a var, ref, atom, etc., determines the "safety" or atomicity of the change.

You could however easily modify one of Java's thread-unsafe data structures from multiple threads and end up in a bad place.

Upvotes: 6

Related Questions