`binding` in the context of `future` vs. `Thread.`

I was playing around and stumbled on a case regarding how binding works, which I have difficulty explaining.

(def ^:dynamic *val* :a)

@(future *val*) ;=> :a

@(binding [*val* :b]
   (future *val*)) ;=> :b

From the docs on future:

Takes a body of expressions and yields a future object that will
invoke the body in another thread […]

If the body is invoked in another thread, should last expression above not have returned :a? Quoting from clojure.org:

Vars provide a mechanism to refer to a mutable storage location that can be dynamically rebound (to a new storage location) on a per-thread basis.

It works as expected when using raw threads, like so:

(def ^:dynamic *var* :a)

@(let [p (promise)]
   (.start (Thread. (fn [] (deliver p *val*))))
   p) ;=> :a

@(binding [*val* :b]
   (let [p (promise)]
     (.start (Thread. (fn [] (deliver p *val*))))
     p)) ;=> :a

Can anyone help me explain why the use of future seems like it keeps the dynamic rebinding of the var?

Edit: The above is using Clojure version 1.10.0

After a little more digging, I found the explanation: It is because of the binding-conveyor-fn used in the internals of future, which moves the local binding to the future thread.

Edit: This is actually pretty cool! Agent’s send does the same thing.

I agree with you that it’s counterintuitive and the documentation is indeed wrong when it says dynamic vars are thread local. I used to think that as well, then I figured out how they actually work and now I don’t use them anymore. If you need thread local state, use java.lang.ThreadLocal. It’s straightforward, it won’t cheat on you and it will be faster.

You may find these posts relevant :


As far as I know, implicit binding conveyance is a hidden feature of future, send and core.async's go. Note that clojurescript doesn’t have this quirk, dynamic vars will behave the way you expect.

I’ve not gathered many insights about why it’s been designed like this. In fact, in an early talk, Rich explicitly took position against binding propagation to other threads. The most plausible reason why this behavior was changed may be the fact that thread-local state is often abused to define implicit function arguments (e.g *out* is an implicit argument of prn). Unlike explicit function arguments, if you close over a dynamic var you won’t capture its current value. Thus, if you want to fully emulate the behavior of local bindings, you need a way to manipulate thread-local context and that’s why we have this complicated binding frame mechanism.

I’d say the documentation is right in that dynamic vars are thread local. But independently, future, agent, etc doing binding propagation which allows you to relocate your current thread local bindings to another thread. This is an under-appreciated feature (possibly because it is under-documented!) of future/agent/etc and is hugely useful in allowing you to background an operation while retaining your local dynamic context.

2 Likes

I never understood why dynamic vars weren’t implemented using InheritableThreadLocal. That way, binding conveyance would have been automatic in all child thread context and binding would be consistently dynamic.

@leonoel I think the truth is, dynamic extent is what Clojure tried to have, which isn’t the same as ThreadLocal. That said, the implementation introduced edge cases, so the doc string reveals some of its implementation details so you can understand the edge cases around it.

Edit: For those interested: https://github.com/jiacai2050/inheritable-var re-implements dynamic vars using TransmittableThreadLocal, which makes all child thread, and newly pooled thread have automatic conveyance of the parent binding when inside the binding block. Thus being more consistent with the expectations of dynamic extent.

This part of the implementation pre-dates my involvement in Clojure dev so I can’t really add any insight on the design aspects of binding conveyance vs InheritableThreadLocal.

I think one difference is that we only sometimes want binding conveyance - if you just start a thread and run something in it, you don’t necessarily want conveyance. If dynamic vars used InheritableThreadLocal, you’d always have conveyance.

1 Like

I can honestly say that I have never needed thread-local state, and have always avoided the dynamic rebinding of vars. The reason I stumbled upon this now is that I started experimenting with the special library, and had the thought that it would be far more useful if the bindings were available in any threads creating in a managed context. Since I knew of the theoretical limitation of binding, I was positively surprised by the fact that it worked like I would have hoped. At least on the surface.

I have looked at your missionary library, @leonoel, and I really like where that is going. I quickly hacked it to convey bindings on the via function, but the cloroutine sp functionality was not as easily hacked. I am guessing that is because you have used ThreadLocal? I will get to the point now: I do really think it would be useful for the functionality in missionary and cloroutine to propagate the bindings.

Thank you all for your insights.

I see the binding more as a way of creating the dynamic scope than a thread-local binding, hence the part about thread-local in the docs has always seemed weird to me. Now that I know more of both the how and the why, it makes sense to me that bindings are propagated in almost all cases.

People would be helped by more clarity about this in the Clojure API docs. @mdiin would you like to make an entry in the Clojure Jira?

P.S.: “binding conveyance” would be a good key phrase for the issue title, to make the new issue findable together with this one: https://dev.clojure.org/jira/browse/CLJ-2214 “Add binding conveyance to reducers”!

2 Likes

I will make an entry for added clarity of the binding conveyance in jira this weekend.

Its possible there are cases you wouldnt want conveyance. I can’t really think of any though. Also, any case that wouldn’t convey would just be breaking dynamic extent. If the point is to offer dynamic scope and extent, it means you want the binding to be accessible to everything inside the binding s-expression and for the value to be bound from the time we enter the s-expr to the time we leave it.

Maybe InheritableThreadLocal has some other issues I don’t know about, edge cases in the opposite direction. I trust Rich Hickey probably made all the right compromises here.

This is exactly what I am thinking. I would love some details about the reasoning behind the implementation from someone involved in that process. I can see why it is not something that can be changed at this point, however, since that would be a breaking change at a very low level.

When to use thread local state

Thread local state is all about imperative programming.

Resource reuse

Let’s say I need to perform some heavy string concatenation work, and I want to cut down garbage generation to increase performance. clojure.core/str instanciates a new buffer for each call, although its extent is strictly limited to the evaluation of the function. Buffers could be reused across invocations, but we need to ensure a given buffer won’t be accessed concurrently. ThreadLocal provides exactly this guarantee.

(def ^ThreadLocal tl
  (ThreadLocal/withInitial
    (reify java.util.function.Supplier
      (get [_] (StringBuilder.)))))

(defn my-str [x & ys]
  (let [^StringBuilder sb (.get tl)]
    (. sb setLength 0)
    (loop [x x ys ys]
      (. sb append x)
      (if ys
        (recur (first ys) (next ys))
        (. sb toString)))))

All good. my-str is still pure from the outside, it doesn’t allocate more objects than necessary and it’s race-condition free.

Dynamic scope

STM transactions are a good example of resources with dynamic extent. Because the lifecycle of a transaction must be fully managed by the underlying engine, it would not be a good idea to expose it directly in the API. Instead, we have to wrap the set of actions to be performed in a dosync block, and let the engine take care of running it within an appropriate transactional context. Transactions are never actually exposed, they’re just implicitly spanning the execution of the expression block. Indefinite scope + dynamic extent => dynamic scope.

(def names (ref []))
(dosync
  (alter names conj "zack")                  ;; ok
  @(future (alter names conj "shelley")))    ;; error, not in transaction

What allows the transaction object to never be exposed to the user is thread-local context. During the execution of a dosync block, any STM action performs a lookup to this variable to get the current transaction and updates it to keep track of intents. The transaction is invisible to other threads so any attempt to escape synchronous scope of execution is doomed to fail.

Why dynamic vars won’t help

If you take the approximation (= dynamic-vars thread-local) for granted, you may be tempted to implement this kind of stuff with dynamic vars. Unfortunately you can’t, because binding conveyance is allowed to break thread locality, effectively exposing your unsynchronized objects to race conditions. Don’t do that.

When NOT to use thread local state

Now, you can say : OK, I will use thread-local state with pure values only. Then I can safely capture current context and restore it later in another thread, nothing wrong can happen. I can use this trick to provide implicit arguments to my functions and my code can be much more concise.

The problem is, adding implicit context breaks referential transparency, increases mental overhead and fights against common clojure idioms. There’s just too many ways things can go wrong. Lazy sequences, and laziness in general, will escape thread-locality. Lambdas won’t capture thread-local context.

You should always prefer functional style and explicit arguments. If you think the arity of your function is too high, use maps. The dynamic var system relies on maps, anyways.

The special library’s design is really typical of how anti-FP this pattern is. First, the library must eagerify results to make sure each evaluation happens within the extent of the function call (synchronously), which means you can’t use infinite sequences anymore. Then, if you want to declare a lambda inside a managed context, you have to be aware the context won’t be propagated to the lambda. Basically, you introduced non-determinism in your function and you’re not doing FP anymore.

(require '[special.core :refer [condition]])
(defn non-deterministic [n]
  (for [i (range n)]
    (if (odd? i)
      (condition :odd i :normally 100)
      i)))

A functional approach would be something like this :

(defn non-deterministic [n]
  (fn [condition]
    (for [i (range n)]
      (if (odd? i)
        (condition :odd i :normally 100)
        i))))

Non-determinism is now explicit, functions are pure, you can close over your context, you can use infinite sequences, you can test in isolation. Sure, it’s more characters to type, but you need to be aware of the trade-off and ask yourself if what you get is worth giving away referential transparency.

How all this relates to green threading

A green thread, in the broad sense, is an identity holding a logical sequential process, represented in a way that allows execution to be fully managed in user-space. Because green threads run on actual threads, thread-local state can be leveraged to keep track of the process currently running (but that’s really an implementation detail).

Now some questions arise about dynamic vars.

  • When a green thread is spawned, should it inherit current thread-local state ? go blocks do that in JVM clojure, consistently with future et al, but not in clojurescript.
  • If I declare a binding block inside a go block, and there’s an asynchronous boundary inside, what should happen ? Should dynamic context be teared down before the asynchronous boundary, and restored after ? Should I be allowed to set! bindings in this case ? How is it actually done in core.async ? What about clojurescript ?

To be honest I don’t think there’s an obvious answer to all these questions, because it’s not clear at all which problem dynamic vars are trying to solve. missionary's design adds even more questions because process declaration is decoupled from execution. So that’s why I chose to ignore them completely. This resulted in a sound, performant, platform-consistent behavior and I’m really happy with it. When I need to use a function relying on dynamic vars, I wrap it in a function taking explicit arguments. I may change my mind in the future if I see a convincing rationale about the current design of dynamic vars, but frankly I doubt it will ever happen.

2 Likes

Thank you for the detailed response. I was aware of the special library’s design and problematic trade-offs, as well as their “correct” functional solutions, the interesting part of it is the convenience that can be provided by sparing use of dynamic scope. But I agree with you that even small amounts of magic in a code base is very likely to lead to troubles down the road.

I have opened a Jira ticket here: https://dev.clojure.org/jira/browse/CLJ-2476

They’re not a concurrency construct. In prior Lisps, multi-threading didn’t exist, so dynamic scope and extent was easy to implement. In Clojure it was difficult, so ThreadLocal was used.

At least that’s my impression. They’re use case would be to set a value from the time of entering a block to leaving the block, which is visible to all the code pointed to after entering the block, including deeply nested blocks within.

In theory, the value should thus be visible and set throughout the entire logical thread of execution which entered the block.

Now, if that conceptual “thread” of execution is not supposed to terminate after leaving the block, things get a bit grey. For example, lazy sequence have this issue. Async processes could as well.

A concrete usage would be to set options for things inside the block. Say set a log true binding. So ideally, everything the programmer perceives as happening within the block should see the binding and its value.

Async, lazy and multi-threading makes that more complicated and requires hammock time.

2 Likes

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.