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
; }
文章转自庄周梦蝶 ,原文发布时间2010-07-23
相关资源:敏捷开发V1.0.pptx