Clojure的并发(五)binding和let

    xiaoxiao2024-03-23  114

    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
    最新回复(0)