pulley
is the Positronic utility libraries.
It is a collection of relatively small, simple libraries
developed by Positronic Solutions, LLC.
It is our pleasure to make them available to the public.
pulley.thread-local
is part of the pulley
collection of libraries.
It provides an extension of Java’a ThreadLocal
class
that implements Clojure’s IAtom
interface.
The result is a thread-local reference type
that integrates seamlessly with the rest of Clojure’s reference types.
Clojure already provides dynamic vars which provide a type
of thread-local bindings.
In fact, the Clojure docs describe dynamic vars
using terms such as “thread-bound”.
So why do we need pulley.thread-local
?
While dynamic vars are useful in a lot of cases, they do not really align perfectly with the concept of a thread-local reference. The bindings for dynamic vars are of dynamic extent — they change by virtue of winding / unwinding the control stack. Sometimes we want a thread-local binding, but need more explicit control over its lifetime.
Another issue with dynamic vars is that it is possible
to capture and restore the current dynamic environment
(e.g., bound-fn
).
This is extremely useful in certain situations
where we wish to delay execution of a function
that depends on implicit arguments stored in the dynamic environment.
If we don’t save and restore the dynamic environment in this case,
the wrong bindings will be in place when the function is executed.
The result is a bug.
In some cases, however, it would be a bug to capture certain bindings.
For example, an HTTP server library might wish to provide a binding for the current response stream. For ease of access, this binding could be provided via a top-level variable. Obviously, this variable must be bound to thread-local values. Otherwise, multiple threads would be unable to reliably access the output stream associated with the current request. On the other hand, if a dynamic var is used, it would be easy to inadvertantly capure the binding and restore it in a context where the stream is no longer valid. In situtations like this, it may be more appropriate to explicitly control the capture and restoration of a specific variable, rather than lumping it together with the entire dynamic context.
In short, while Clojure’s dynamic vars are very useful in a number of cases,
there are times when you simply want a thread-local binding
without the restrictions of dynamic vars.
pulley.thread-local
exists for these cases.
Of course, there’s a trade-off here. What you gain in control, you lose in the need for more explicit management. Choose wisely and tread softly….
To use pulley.thread-local
, first add the following dependency:
[com.positronic-solutions/pulley.thread-local "0.1.0"]
Then require
the com.positronic-solutions.pulley.thread-local
namespace.
(require '[com.positronic-solutions.pulley.thread-local :as thread-local])
pulley.thread-local
provides a single function, thread-local
,
which constructs an atom-like reference object.
The value dereferenced depends on the current thread.
The value passed to thread-local
will be the default value
(i.e., the initial value associated with a new thread).
(def foo (thread-local/thread-local :foo))
Once constructed, we can use the same operations we would use
on a Clojure atom
.
(deref foo)
;; => :foo
;; Re-binding foo in a different thread only affects that thread
(-> (new Thread (fn []
(reset! foo :bar)
(println @foo)))
(. (start)))
;; => :bar
;; The binding on other threads is not affected
@foo
;; => :foo
pulley.thread-local
is licensed
under the GNU Lesser General Public License, version 3 or later.
pulley.thread-local
is written in a Literate Programming format.
All source code for the library is contained in this document
(specifically this section).
The source code can be extracted via Emacs Org mode and Org babel.
pulley.thread-local
’s interface consists of a single function
within a single namespace:
(ns com.positronic-solutions.pulley.thread-local)
(defn thread-local [root-value]
(new com.positronic_solutions.pulley.thread_local.RootedThreadLocal root-value))
As you can see, virtually all functionality is implemented
by the RootedThreadLocal
class.
The RootedThreadLocal
class implements the heart of pulley.thread-local
.
It extends java.lang.ThreadLocal
(to override the initialValue
method),
and implements IDeref
and IAtom
from clojure.lang
.
It is necessary to implement this in Java, because:
- We must override
ThreadLocal
’sinitialValue
method to produce the “root” value, sinceThreadLocal
’s implementation simply returnsnull
. - Clojure’s
deftype
,reify
, etc. do not support class inheritance. (We could useproxy
, but that has a performance cost.) So it is not possible to extendThreadLocal
using Clojure.
While we could wrap RootedThreadLocal
and implement IDeref
and IAtom
in Clojure
(e.g., with reify
or defype
),
there seems to be little (if any) benefit to doing so.
The code is trival enough to implement in Java
without any significant disadvantage.
On the other hand, exposing the RootedThreadLocal
object directly
allows Java code to utilize the ThreadLocal
interface with it.
This could be beneficial for interop purposes.
package com.positronic_solutions.pulley.thread_local;
public class RootedThreadLocal extends ThreadLocal
implements clojure.lang.IDeref,
clojure.lang.IAtom {
private final Object root_value;
protected Object initialValue(){
return this.root_value;
}
public RootedThreadLocal(Object root_value){
this.root_value = root_value;
}
public Object deref(){
return this.get();
}
public Object swap(clojure.lang.IFn f){
final Object old_value = this.deref();
final Object new_value = f.invoke(old_value);
return this.reset(new_value);
}
public Object swap(clojure.lang.IFn f, Object x){
final Object old_value = this.deref();
final Object new_value = f.invoke(old_value, x);
return this.reset(new_value);
}
public Object swap(clojure.lang.IFn f, Object x, Object y){
final Object old_value = this.deref();
final Object new_value = f.invoke(old_value, x, y);
return this.reset(new_value);
}
public Object swap(clojure.lang.IFn f,
Object x,
Object y,
clojure.lang.ISeq args){
final Object old_value = this.deref();
final Object new_value = f.applyTo(args.cons(y).cons(x).cons(old_value));
return this.reset(new_value);
}
public boolean compareAndSet(Object oldv, Object newv){
final Object v = this.deref();
if(clojure.lang.Util.equiv(v, oldv)){
this.reset(newv);
return true;
}
else{
return false;
}
}
public Object reset(Object newval){
this.set(newval);
return newval;
}
}
The Leiningen project file is also very simple:
(defproject com.positronic-solutions/pulley.thread-local "0.1.0"
:description "Truly thread-local bindings for Clojure"
:url "https://github.com/positronic-solutions/pulley.thread-local"
:license {:name "GNU Lesser General Public License, v. 3 or later"
:url "http://www.gnu.org/licenses/lgpl.html"
:distribution :repo}
:dependencies [[org.clojure/clojure "1.8.0"]]
:source-paths ["src/clj"]
:java-source-paths ["src/java"])
Since we have both Clojure and Java source,
we split the code into src/clj
and src/java
.
Therefore, we must add appropriate values
for :source-paths
and :java-source-paths
.