莊周夢蝶

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

          Clojure的并發(fā)(一) Ref和STM

          Posted on 2010-07-14 02:34 dennis 閱讀(7883) 評論(8)  編輯  收藏 所屬分類: 動(dòng)態(tài)語言javaClojure

          Clojure 的并發(fā)(一) Ref和STM
          Clojure 的并發(fā)(二)Write Skew分析
          Clojure 的并發(fā)(三)Atom、緩存和性能
          Clojure 的并發(fā)(四)Agent深入分析和Actor
          Clojure 的并發(fā)(五)binding和let
          Clojure的并發(fā)(六)Agent可以改進(jìn)的地方
          Clojure的并發(fā)(七)pmap、pvalues和pcalls
          Clojure的并發(fā)(八)future、promise和線程

              Clojure處理并發(fā)的思路與眾不同,采用的是所謂STM的模型——軟事務(wù)內(nèi)存。你可以將STM想象成數(shù)據(jù)庫,只不過是內(nèi)存型的,它只支持事務(wù)的ACI,也就是原子性、一致性、隔離性,但是不包括持久性,因?yàn)闋顟B(tài)的保存都在內(nèi)存里。

              Clojure的并發(fā)API分為四種模型:
          1、管理協(xié)作式、同步修改可變狀態(tài)的Ref
          2、管理非協(xié)作式、同步修改可變狀態(tài)的Atom
          3、管理異步修改可變狀態(tài)的Agent
          4、管理Thread local變量的Var。

              下面將對這四部分作更詳細(xì)的介紹。

          一、Ref和STM

           1、ref:

          通過ref函數(shù)創(chuàng)建一個(gè)可變的引用(reference),指向一個(gè)不可變的對象:
          (ref x)

          例子:創(chuàng)建一個(gè)歌曲集合:
          (def song (ref #{}))

          2、deref和@:
           取引用的內(nèi)容,解引用使用deref函數(shù)
          (deref song)

          也可以用reader宏@:
          @song

          3、ref-set和dosync:


          改變引用指向的內(nèi)容,使用ref-set函數(shù)
          (ref-set ref new-value)

          如,我們設(shè)置新的歌曲集合,加入一首歌:
          (ref-set song #{"Dangerous"})
          但是這樣會(huì)報(bào)錯(cuò):
          java.lang.IllegalStateException: No transaction running (NO_SOURCE_FILE:0)

          這是因?yàn)橐檬强勺兊模瑢顟B(tài)的更新需要進(jìn)行保護(hù),傳統(tǒng)語言的話可能采用鎖,Clojure是采用事務(wù),將更新包裝到事務(wù)里,這是通過dosync實(shí)現(xiàn)的:
          (dosync (ref-set song #{"Dangerous"}))

          dosync的參數(shù)接受多個(gè)表達(dá)式,這些表達(dá)式將被包裝在一個(gè)事務(wù)里,事務(wù)支持ACI:
          (1)Atomic,如果你在事務(wù)里更新多個(gè)Ref,那么這些更新對事務(wù)外部來說是一個(gè)獨(dú)立的操作。
          (2)Consistent,Ref的更新可以設(shè)置 validator,如果某個(gè)驗(yàn)證失敗,整個(gè)事務(wù)將回滾。
          (3)Isolated,運(yùn)行中的事務(wù)無法看到其他事務(wù)部分完成的結(jié)果。

          dosync更新多個(gè)Ref,假設(shè)我們還有個(gè)演唱者Ref,同時(shí)更新歌曲集合和演唱者集合:
          (def singer (ref #{}))
          (dosync (ref
          -set song #{"Dangerous"})
                         (ref
          -set singer #{"MJ"}) )

          @song      
          =>  #{"Dangerous"}
          @singer    
          =>  #{"MJ"}

          4、alter:
          完全更新整個(gè)引用的值還是比較少見,更常見的更新是根據(jù)當(dāng)前狀態(tài)更新,例如我們向歌曲集合添加一個(gè)歌曲,步驟大概是先查詢集合內(nèi)容,然后往集合里添加歌曲,然后更新整個(gè)集合:
          (dosync (ref-set song (conj @song "heal the world")))

          查詢并更新的操作可以合成一步,這是通過alter函數(shù):
          (alter ref update-fn & args)

          alter接收一個(gè)更新的函數(shù),函數(shù)將在更新的時(shí)候調(diào)用,傳入當(dāng)前狀態(tài)值并返回新的狀態(tài)值,因此上面的例子可以改寫為:
           (dosync (alter song conj "heal the world"))

          這里使用conj而非cons是因?yàn)閏onj接收的第一個(gè)參數(shù)是集合,也就是當(dāng)前狀態(tài)值,而cons要求第一個(gè)參數(shù)是將要加入的元素。

          5、commute:
            commute函數(shù)是alter的變形,commute顧名思義就是要求update-function是可交換的,它的順序是可以任意排序。commute的允許的并發(fā)程度比alter更高一些,因此性能會(huì)更好。但是由于commute要求update-function是可交換的,并且會(huì)自動(dòng)重排序,因此如果你的更新要求順序性,那么commute是不能接受的,commute僅可用在對順序性沒有要求或者要求很低的場景:例如更新聊天窗口的聊天信息,由于網(wǎng)絡(luò)延遲的因素和個(gè)人介入的因素,聊天信息可以認(rèn)為是天然排序,因此使用commute還可以接受,更新亂序的可能性很低。
            另一個(gè)例子就不能使用commute了,如實(shí)現(xiàn)一個(gè)計(jì)數(shù)器:
          (def counter (ref 0))

            實(shí)現(xiàn)一個(gè)next-counter函數(shù)獲取計(jì)數(shù)器的下一個(gè)值,我們先使用commute實(shí)現(xiàn):
          (defn next-counter [] (dosync (commute counter inc)))

             這個(gè)函數(shù)很簡單,每次調(diào)用inc遞增counter的值,接下來寫個(gè)測試用例:啟動(dòng)50個(gè)線程并發(fā)去獲取next counter:
          (dotimes [_ 50] (.start (Thread. #(println (next-counter)))))
            
             這段代碼稍微解釋下,dotimes是重復(fù)執(zhí)行50次,每次啟動(dòng)new并啟動(dòng)一個(gè)Thread,這個(gè)Thread里干了兩件事情:調(diào)用next-counter,打印調(diào)用結(jié)果,第一個(gè)版本的next-counter執(zhí)行下,這是其中一次輸出的截取:
          23
          23
          23

          23
          23
          23
          23
          23
          23
          23
          23
          23
          28
          23
          21
          23
          23
          23
          23
          25
          28

          可以看到有很多的重復(fù)數(shù)值,這是由于重排序?qū)е率聞?wù)結(jié)束后的值不同,但是你查看counter,確實(shí)是50:
          @counter  => 50

          證明更新是沒有問題的,問題出在commute的返回值上。

          如果將next-counter修改為alter實(shí)現(xiàn):
          (defn next-counter [] (dosync (alter counter inc)))

          此時(shí)再執(zhí)行測試用例,可以發(fā)現(xiàn)打印結(jié)果完全正確了:
          ……
          39
          41
          42
          45
          27
          46
          47
          44
          48
          43
          49
          40
          50

          查看counter,也是正確更新到50了:
          @counter => 50

          最佳實(shí)踐:通常情況下,你應(yīng)該優(yōu)先使用alter,除非在遇到明顯的性能瓶頸并且對順序不是那么關(guān)心的時(shí)候,可以考慮用commute替換。

          6、validator:
             類似數(shù)據(jù)庫,你也可以為Ref添加“約束”,在數(shù)據(jù)更新的時(shí)候需要通過validator函數(shù)的驗(yàn)證,如果驗(yàn)證不通過,整個(gè)事務(wù)將回滾。添加validator是通過ref函數(shù)傳入metadata的map實(shí)現(xiàn)的,例如我們要求歌曲集合添加的歌曲名稱不能為空:
          (def validate-song
               (partial every? #(not (nil?
          %))))

          (def song (ref #{} :validator validate
          -song))

          validate-song是一個(gè)驗(yàn)證函數(shù),partial返回某個(gè)函數(shù)的半函數(shù)(固定了部分參數(shù),部分參數(shù)沒固定),你可以將partial理解成currying,雖然還是不同的。validate-song調(diào)用every?來驗(yàn)證集合內(nèi)的所有元素都不是nil,其中#(not (nil? %))是一個(gè)匿名函數(shù),%指向匿名函數(shù)的第一個(gè)參數(shù),也就是集合的每個(gè)元素。ref指定了validator為validate-song,那么在每次更新song集合的時(shí)候都會(huì)將新的狀態(tài)傳入validator函數(shù)里驗(yàn)證一下,如果返回false,整個(gè)事務(wù)將回滾:

          (dosync (alter song conj nil))
          java.lang.IllegalStateException: Invalid reference state (NO_SOURCE_FILE:
          0)

          更新失敗,非法的reference狀態(tài),查看song果然還是空的:
          @song => #{}

          更新正常的值就沒有問題:
           (dosync (alter song conj "dangerous"))   => #{"dangerous"}

             
          7、ensure:

            ensure函數(shù)是為了保護(hù)Ref不會(huì)被其他事務(wù)所修改,它的主要目的是為了防止所謂的“寫偏序”(write skew)問題。寫偏序問題的產(chǎn)生跟STM的實(shí)現(xiàn)有關(guān),clojure的STM實(shí)現(xiàn)是基于MVCC(Multiversion Concurrency Control)——多版本并發(fā)控制,對一個(gè)Ref保存多個(gè)版本的狀態(tài)值,在更新的時(shí)候取得當(dāng)前狀態(tài)值的一個(gè)隔離的snapshot,更新是基于snapshot進(jìn)行的。那么我們來看下寫偏序是怎么產(chǎn)生,以一個(gè)比喻來描述:
            想象有一個(gè)系統(tǒng)用于管理美國最神秘的軍事禁區(qū)——51區(qū)的安全巡邏,你有3個(gè)營的士兵,每個(gè)營45個(gè)士兵,并且你需要保證總體巡邏的士兵人數(shù)不能少于100個(gè)人。假設(shè)有一天,有兩個(gè)指揮官都登錄了這個(gè)管理系統(tǒng),他們都想從某個(gè)軍營里抽走20個(gè)士兵,假設(shè)指揮官A想從1號軍營抽走,指揮官B想要從2號軍營抽走士兵,他們同時(shí)執(zhí)行下列操作:
          Admin 1if ((G1 - 20+ G2 + G3) > 100 then dispatchPatrol

          Admin 
          2if (G1 + (G2 - 20+ G3) > 100 then dispatchPatrol

          我們剛才提到,Clojure的更新是基于隔離的snapshot,一個(gè)事務(wù)的更改無法看到另一個(gè)事務(wù)更改了部分的結(jié)果,因此這兩個(gè)操作都因?yàn)闈M足(45-20)+45+45=115的約束而得到執(zhí)行,導(dǎo)致實(shí)際抽調(diào)走了40個(gè)士兵,只剩下95個(gè)士兵,低于設(shè)定的安全標(biāo)準(zhǔn)100人,這就是寫偏序現(xiàn)象。
            寫偏序的解決就很簡單,在執(zhí)行抽調(diào)前加入ensure即可保護(hù)ref不被其他事務(wù)所修改。ensure比(ref-set ref @ref)允許的并發(fā)程度更高一些。


          Ref和STM的介紹暫時(shí)到這里,原理和源碼的解析要留待下一篇文章了。




          評論

          # re: Clojure的并發(fā)(一) Ref和STM  回復(fù)  更多評論   

          2010-07-14 19:09 by clojans
          樓主高手呀!
          但4個(gè)模型前要加個(gè)"管理"形容詞呢?不是只有ref才是協(xié)調(diào)管理的嗎?

          # re: Clojure的并發(fā)(一) Ref和STM  回復(fù)  更多評論   

          2010-07-14 19:10 by clojans
          樓主高手呀!
          但4個(gè)模型前為什么要加個(gè)"管理"形容詞呢?不是只有ref才是協(xié)調(diào)管理的嗎?

          --1 Lou寫錯(cuò)了:-(

          # re: Clojure的并發(fā)(一) Ref和STM  回復(fù)  更多評論   

          2010-07-14 19:12 by nmb
          樓主晚上不睡覺嗎?

          # re: Clojure的并發(fā)(一) Ref和STM  回復(fù)  更多評論   

          2010-07-14 20:30 by dennis
          @clojans
          非形容詞,而是動(dòng)詞

          # re: Clojure的并發(fā)(一) Ref和STM  回復(fù)  更多評論   

          2011-08-19 13:42 by sw2wolf
          不錯(cuò)! 加深我對HASKELL相關(guān)概念的理解

          # re: Clojure的并發(fā)(一) Ref和STM  回復(fù)  更多評論   

          2013-09-25 13:14 by paomian
          莊莊我愛你!

          # re: Clojure的并發(fā)(一) Ref和STM  回復(fù)  更多評論   

          2014-03-26 13:11 by flyfoxs
          commute 會(huì)導(dǎo)致執(zhí)行修改ref的函數(shù)執(zhí)行2次. 這樣設(shè)計(jì)的目的是為什么,難道這2次執(zhí)行又一次不是多余的嗎?

          # re: Clojure的并發(fā)(一) Ref和STM[未登錄]  回復(fù)  更多評論   

          2014-07-04 18:11 by sure
          commute 重排序的到底是什么?
          主站蜘蛛池模板: 洛川县| 桓仁| 贵港市| 涟源市| 精河县| 楚雄市| 乃东县| 青冈县| 澎湖县| 揭西县| 普兰县| 吉林省| 巨野县| 青铜峡市| 章丘市| 甘谷县| 鄂尔多斯市| 繁昌县| 逊克县| 桂平市| 昌宁县| 西城区| 灌云县| 陕西省| 邢台市| 贵溪市| 营口市| 阜宁县| 普洱| 易门县| 调兵山市| 若羌县| 宁明县| 蒙阴县| 行唐县| 易门县| 北辰区| 余江县| 桂平市| 密云县| 额济纳旗|