Mark McGranaghan

Environment Passing in Clojure

July 11 2010

Clojure’s vars provide a nice mechanism for managing dynamically bound state, but you need to be careful with vars when moving computations across thread boundaries. For example, suppose we implement the following timeout function:

(import '(java.util.concurrent Future TimeUnit))

(defn timeout1 [millis op]
  (.get (future-call op) millis TimeUnit/MILLISECONDS))

The function timeout1 sends the operation op to be computed in a separate, timeout-bound thread. If the operation completes within millis milliseconds, timeout1 returns the result of that operation, otherwise it throws an exception:

(timeout1 1000 #(inc 1))
=> 2

(timeout1 1000 #(Thread/sleep 2000))
; throws java.util.concurrent.TimeoutException 

This timeout implementation demonstrates the potential problem with using vars and threads:

(def a-var 3)

(binding [a-var 4] (inc a-var))
=> 5

(timeout1 1000 #(binding [a-var 4] (inc a-var)))
=> 5

(binding [a-var 4] (timeout1 1000 #(inc a-var)))
=> 4

If we rebind a-var within the timeout thread, we get the expected result of 5. But if we rebind a-var outside of the timeout thread, the computation sees the root value of 3 for a-var, not the thread-local value of 4.

To fix this, we need to “pass the environment” of the calling thread to the computation thread:

(defn timeout2 [millis op]
  (let [env    (get-thread-bindings)
        env-op #(with-bindings* env op)]
    (.get (future-call env-op) millis TimeUnit/MILLISECONDS)))

(timeout2 1000 #(binding [a-var 4] (inc a-var)))
=> 5

(binding [a-var 4] (timeout2 1000 #(inc a-var)))
=> 5

Here we capture the environment in the context of the caller and inject it into the timeout thread using with-bindings*. Now that the timeout thread sees the same thread-local environment as the caller, the external binding of a-var works as expected.

Finally, we can extract this pattern into a macro passing-bindings that can be used to pass an environment across thread boundaries:

(defmacro passing-bindings [[in-env-sym] body]
  `(let [env# (get-thread-bindings)
         ~in-env-sym (fn [op#] #(with-bindings* env# op#))]

(defn timeout3 [millis op]
  (passing-bindings [in-env]
    (.get (future-call (in-env op)) millis TimeUnit/MILLISECONDS)))

(binding [a-var 4] (timeout3 1000 #(inc a-var)))
=> 5