查看Clojure并发的源代码
←
Clojure并发
跳转到:
导航
、
搜索
因为以下原因,你没有权限编辑本页:
您刚才请求的操作只有这个用户组中的用户才能使用:
用户
您可以查看并复制此页面的源代码:
== binding和let:线程局部量 == 前面几节已经介绍了Ref、Atom和Agent,其中Ref用于同步协调多个状态变量,Atom只能用于同步独立的状态变量,而Agent则是允许异步的状态更新。这里将介绍下binding,用于线程内的状态的管理。 *binding和let: 当你使用def定义一个var,并传递一个初始值给它,这个初始值就称为这个var的root binding。这个root binding可以被所有线程共享,例如: user=> (def ^:dynamic 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(从clojure 1.3开始,var必须声明为dynamic才可以做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 *Binding的妙用: Binding可以用于实现类似AOP编程这样的效果,例如我们有个fib函数用于计算阶乘: <pre> user=> (defn ^:dynamic fib [n] (loop [ n n r 1] (if (= n 1) r (recur (dec n) (* n r))))) </pre> 然后有个call-fibs函数调用fib函数计算两个数的阶乘之和: <pre> user=> (defn call-fibs [a b] (+ (fib a) (fib b))) #'user/call-fibs user=> (call-fibs 3 3) 12 </pre> 现在我们有这么个需求,希望使用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,这其实是引入了副作用(这也是Clojure 1.3将var不再默认为dynamic的原因)。因此对于binding的这种使用方式要相当慎重。这其实有点类似Ruby中的open class做monkey patch,你可以随时随地地改变对象的行为,但是你要承担相应的后果。 *binding和let的实现上的区别: 前面已经提到,let其实是词法作用域的对变量的“遮蔽”,它并非重新绑定变量值,而binding则是在变量的root binding之外在线程的ThreadLocal内存储了一个绑定值,变量值的查找顺序是先查看ThreadLocal有没有值,有的话优先返回,没有则返回root binding。下面将从Clojure源码角度分析。 变量在clojure是存储为Var对象,它的内部包括: <pre> //这是变量的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 </pre> 通过def定义一个变量,相当于生成一个Var对象,并将root设置为初始值。 先看下let表达式生成的字节码(各个Clojure版本生成的字节吗会稍有不同,大体是一致的): <pre> (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 } </pre> 可以看到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是一个宏,展开之后等价于: <pre> (let [] (push-thread-bindings (hash-map (var foo) 3)) (try foo (finally (pop-thread-bindings)))) </pre> 首先是将binding的绑定列表转化为一个hash-map,其中key为变量foo,值为3。函数push-thread-bindings: <pre> (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)); } </pre> pushThreadBindings是将绑定关系放入一个新的frame(新的context),并存入ThreadLocal变量dvals。pop-thread-bindings函数相反,弹出一个Frame,它实际调用的是Var.popThreadBindings静态方法: <pre> 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); } </pre> 在执行宏的body表达式,也就是取foo值的时候,实际调用的是Var.deref静态方法取变量值: <pre> 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)); } </pre> 看到是先尝试从ThreadLocal找: <pre> final Box getThreadBinding(){ if(count.get() > 0) { IMapEntry e = dvals.get().bindings.entryAt(this); if(e != null) return (Box) e.val(); } return null; } </pre> 找不到,如果有初始值就返回初始的root binding,否则抛出异常:Var user/foo is unbound. binding表达式最后生成的字节码,做的就是上面描述的这些函数调用,有兴趣地可以自行分析。
返回到
Clojure并发
。
个人工具
登录
名字空间
页面
讨论
变换
查看
阅读
查看源代码
查看历史
操作
搜索
导航
首页
社区专页
新闻动态
最近更改
随机页面
帮助
工具箱
链入页面
相关更改
特殊页面