Clojure
的并發(一) Ref和STM
Clojure
的并發(二)Write Skew分析
Clojure
的并發(三)Atom、緩存和性能
Clojure
的并發(四)Agent深入分析和Actor
Clojure
的并發(五)binding和let
Clojure的并發(六)Agent可以改進的地方
Clojure的并發(七)pmap、pvalues和pcalls
Clojure的并發(八)future、promise和線程
五、binding和let
前面幾節已經介紹了Ref、Atom和Agent,其中Ref用于同步協調多個狀態變量,Atom只能用于同步獨立的狀態變量,而Agent則是允許異步的狀態更新。這里將介紹下binding,用于線程內的狀態的管理。
1、binding和let:
當你使用def定義一個var,并傳遞一個初始值給它,這個初始值就稱為這個var的root binding。這個root binding可以被所有線程共享,例如:
user=> (def foo 1)
#'user/foo
那么對于變量foo來說,1是它的root binding,這個值對于所有線程可見,REPL的主線程可見:
user=> foo
1
啟動一個獨立線程查看下foo的值:
user=> (.start (Thread. #(println foo)))
nil
1
可以看到,1這個值對于所有線程都是可見的。
但是,利用binding宏可以給var創建一個thread-local級別的binding:
(binding [bindings] & body)
binding的范圍是動態的,binding只對于持有它的線程是可見的,直到線程執行超過binding的范圍為止,binding對于其他線程是不可見的。
user=> (binding [foo 2] foo)
2
粗看起來,binding和let非常相似,兩者的調用方式近乎一致:
user=> (let [foo 2] foo)
2
從一個例子可以看出兩者的不同,定義一個print-foo函數,用于打印foo變量:
user=> (defn print-foo [] (println foo))
#'user/print-foo
foo不是從參數傳入的,而是直接從當前context尋找的,因此foo需要預先定義。分別通過let和binding來調用print-foo:
user=> (let [foo 2] (print-foo))
1
nil
可以看到,print-foo仍然打印的是初始值1,而不是let綁定的2。如果用binding:
user=> (binding [foo 2] (print-foo))
2
nil
print-foo這時候打印的就是binding綁定的2。這是為什么呢?這是由于let的綁定是靜態的,
它并不是改變變量foo的值,而是用一個詞法作用域的foo“遮蔽”了外部的foo的值。但是print-foo卻是
查找變量foo的值,因此let的綁定對它來說是沒有意義的,嘗試利用set!去修改let的foo:
user=> (let [foo 2] (set! foo 3))
java.lang.IllegalArgumentException: Invalid assignment target (NO_SOURCE_FILE:12)
Clojure告訴你,let中的foo不是一個有效的賦值目標
,foo是不可變的值。set!可以修改binding的變量:
user=> (binding [foo 2] (set! foo 3) (print-foo))
3
nil
2、Binding的妙用:
Binding可以用于實現類似AOP編程這樣的效果,例如我們有個fib函數用于計算階乘:
user=> (defn fib [n]
(loop [ n n r 1]
(if (= n 1)
r
(recur (dec n) (* n r)))))
然后有個call-fibs函數調用fib函數計算兩個數的階乘之和:
user=> (defn call-fibs [a b]
(+ (fib a) (fib b)))
#'user/call-fibs
user=> (call-fibs 3 3)
12
現在我們有這么個需求,希望使用memoize來加速fib函數,我們不希望修改fib函數,因為這個函數可能其他地方用到,其他地方不需要加速,而我們希望僅僅在調用call-fibs的時候加速下fib的執行,這時候可以利用binding來動態綁定新的fib函數:
user=> (binding [fib (memoize fib)]
(call-fibs 9 10))
3991680
在沒有改變fib定義的情況下,只是執行call-fibs的時候動態改變了原fib函數的行為,這不是跟AOP很相似嗎?
但是這樣做已經讓call-fibs這個函數
不再是一個“純函數”,所謂“純函數”是指一個函數對于相同的參數輸入永遠返回相同的結果,但是由于binding可以動態隱式地改變函數的行為,導致相同的參數可能返回不同的結果,例如這里可以將fib綁定為一個返回平方值的函數,那么call-fibs對于相同的參數輸入產生的值就改變了,取決于當前的context,這其實是引入了副作用。因此對于binding的這種使用方式要相當慎重。這其實有點類似Ruby中的open class做monkey patch,你可以隨時隨地地改變對象的行為,但是你要承擔相應的后果。
3、binding和let的實現上的區別:
前面已經提到,let其實是詞法作用域的對變量的“遮蔽”,它并非重新綁定變量值,而binding則是在變量的root binding之外在線程的ThreadLocal內存儲了一個綁定值,
變量值的查找順序是先查看ThreadLocal有沒有值,有的話優先返回,沒有則返回root binding。下面將從Clojure源碼角度分析。
變量在clojure是存儲為Var對象,它的內部包括:
//這是變量的ThreadLocal值存儲的地方
static ThreadLocal<Frame> dvals = new ThreadLocal<Frame>(){
protected Frame initialValue(){
return new Frame();
}
};
volatile Object root; //這是root binding
public final Symbol sym; //變量的符號
public final Namespace ns; //變量的namespace
通過def定義一個變量,相當于生成一個Var對象,并將root設置為初始值。
先看下let表達式生成的字節碼:
(let [foo 3] foo)
字節碼:
public class user$eval__4349 extends clojure/lang/AFunction {
// compiled from: NO_SOURCE_FILE
// debug info: SMAP
eval__4349.java
Clojure
*S Clojure
*F
+ 1 NO_SOURCE_FILE
NO_SOURCE_PATH
*L
0#1,1:0
*E
// access flags 25
public final static Ljava/lang/Object; const__0
// access flags 9
public static <clinit>()V
L0
LINENUMBER 2 L0
ICONST_3
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
PUTSTATIC user$eval__4349.const__0 : Ljava/lang/Object;
RETURN
MAXSTACK = 0
MAXLOCALS = 0
// access flags 1
public <init>()V
L0
LINENUMBER 2 L0
L1
ALOAD 0
INVOKESPECIAL clojure/lang/AFunction.<init> ()V
L2
RETURN
MAXSTACK = 0
MAXLOCALS = 0
// access flags 1
public invoke()Ljava/lang/Object; throws java/lang/Exception
L0
LINENUMBER 2 L0
GETSTATIC user$eval__4349.const__0 : Ljava/lang/Object;
ASTORE 1
L1
ALOAD 1
L2
LOCALVARIABLE foo Ljava/lang/Object; L1 L2 1
L3
LOCALVARIABLE this Ljava/lang/Object; L0 L3 0
ARETURN
MAXSTACK = 0
MAXLOCALS = 0
}
可以看到foo并沒有形成一個Var對象,而僅僅是將3存儲為靜態變量,最后返回foo的時候,也只是取出靜態變量,直接返回,沒有涉及到變量的查找。let在編譯的時候,將binding作為編譯的context靜態地編譯body的字節碼,body中用到的foo編譯的時候就確定了,沒有任何動態性可言。
再看同樣的表達式替換成binding宏,因為binding只能重新綁定已有的變量,所以需要先定義foo:
user=> (def foo 100)
#'user/foo
user=> (binding [foo 3] foo)
binding是一個宏,展開之后等價于:
(let []
(push-thread-bindings (hash-map (var foo) 3))
(try
foo
(finally
(pop-thread-bindings))))
首先是將binding的綁定列表轉化為一個hash-map,其中key為變量foo,值為3。函數push-thread-bindings:
(defn push-thread-bindings
[bindings]
(clojure.lang.Var/pushThreadBindings bindings))
其實是調用Var.pushThreadBindings這個靜態方法:
public static void pushThreadBindings(Associative bindings){
Frame f = dvals.get();
Associative bmap = f.bindings;
for(ISeq bs = bindings.seq(); bs != null; bs = bs.next())
{
IMapEntry e = (IMapEntry) bs.first();
Var v = (Var) e.key();
v.validate(v.getValidator(), e.val());
v.count.incrementAndGet();
bmap = bmap.assoc(v, new Box(e.val()));
}
dvals.set(new Frame(bindings, bmap, f));
}
pushThreadBindings是將綁定關系放入一個
新的frame(新的context),并存入ThreadLocal變量dvals。
pop-thread-bindings函數相反,彈出一個Frame,它實際調用的是Var.popThreadBindings靜態方法:
public static void popThreadBindings(){
Frame f = dvals.get();
if(f.prev == null)
throw new IllegalStateException("Pop without matching push");
for(ISeq bs = RT.keys(f.frameBindings); bs != null; bs = bs.next())
{
Var v = (Var) bs.first();
v.count.decrementAndGet();
}
dvals.set(f.prev);
}
在執行宏的body表達式,也就是取foo值的時候,實際調用的是Var.deref靜態方法取變量值:
final public Object deref(){
//先從ThreadLocal找
Box b = getThreadBinding();
if(b != null)
return b.val;
//如果有定義初始值,返回root binding
if(hasRoot())
return root;
throw new IllegalStateException(String.format("Var %s/%s is unbound.", ns, sym));
}
看到是先嘗試從ThreadLocal找:
final Box getThreadBinding(){
if(count.get() > 0)
{
IMapEntry e = dvals.get().bindings.entryAt(this);
if(e != null)
return (Box) e.val();
}
return null;
}
找不到,如果有初始值就返回初始的root binding,否則拋出異常:Var user/foo is unbound.
binding表達式最后生成的字節碼,做的就是上面描述的這些函數調用,有興趣地可以自行分析。