莊周夢蝶

          生活、程序、未來
             :: 首頁 ::  ::  :: 聚合  :: 管理

          Clojure的并發(五)binding和let

          Posted on 2010-07-23 23:19 dennis 閱讀(4993) 評論(1)  編輯  收藏 所屬分類: Clojure
          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表達式最后生成的字節碼,做的就是上面描述的這些函數調用,有興趣地可以自行分析。

             

          評論

          # re: Clojure的并發(五)binding和let  回復  更多評論   

          2010-07-26 09:43 by clojans
          樓主應該加上let和binding表達式返回函數的用途和區別就更加完美了。
          主站蜘蛛池模板: 依安县| 连南| 葫芦岛市| 通道| 龙陵县| 江陵县| 集贤县| 宿松县| 南岸区| 安康市| 临城县| 信阳市| 获嘉县| 阿克陶县| 奉新县| 荆门市| 甘洛县| 奉节县| 南溪县| 襄樊市| 永城市| 横峰县| 苗栗县| 谢通门县| 洛阳市| 敖汉旗| 堆龙德庆县| 田林县| 黄骅市| 昌宁县| 北宁市| 那曲县| 贡山| 永城市| 镇安县| 阜城县| 蒙城县| 上饶县| 萍乡市| 永城市| 通山县|