理解Clojure STM 軟體事務性記憶體

來源:互聯網
上載者:User

標籤:

翻譯說明:

英文原文來自:http://java.ociweb.com/mark/stm/article.html

原文包含了一些非STM的知識,也包括STM底層實現的內容,這裡只是翻譯了STM抽象層的內容,自認為這部分比較重要。

翻譯是基於自己能夠理解的方式翻譯的,並非逐句翻譯,目的是理解STM,理解如何調優STM,有逐句翻譯強迫症的同學請不要噴我!

本人是在學習《Clojure編程樂趣》的“壓力之下的 Ref”章節,遇到無法理解minHistory和maxHistory的時候才找到這篇文章的。

Ref是類似Atom和Agent的變數,包含一個所有線程共用的值,Ref只能在STM事務中修改,能夠修改的函數有:ref-set,alter,commute,擷取Ref值的方式是用@讀取器或者deref,讀取的話不需要在事務內部,但是要讀取多個ref的一致性快照的話就需要在事務內部。


Ref是Clojure的STM使用的唯一一種變數類型。


alter和commute的第一個參數是一個Ref,第二個參數是一個能夠返回新值的函數,簡稱參數函數,當參數函數被調用的時候,它會獲得如下參數:當前Ref的值、alter或commute接收到的其它參數,參數函數返回的值作為Ref的新值。


大部分情況下使用alter,使用commute的情況是並發修改Ref,修改順序無關緊要的情況,這與數學裡的commutative(交換律)是一樣的,加法交換律和乘法交換律都是順序無關的,使用commute相當於在說:我現在要啟用一個事務來修改一個Ref,但是我不擔心其它事務會在我這個事務提交前就已經把Ref給改了,因為我在提交的時候還會再擷取一下最新的Ref,並以此再計算一遍,然後提交,而這種做法依然能夠保證最終結果是正確的。


commute的使用情境如集合中增加元素,或者統計一個集合中的資料,最大、最小、平均值等,假設一個Ref持有一個集合,如果2個並發的事務同時向這個集合添加元素,大多數情況下誰先加進去並不重要,事務A比事務B先運行,但事務B在事務A之前往集合裡塞進一個新元素,這時事務A沒有什麼必要全部重試運行整個事務,只需要使用commute在提交的時候,再擷取一次最新的Ref並塞元素進去。再假設Ref持有一個值,這個值是一個集合的最大值,事務A比事務B先運行,但事務B在事務A之前修改了這個最大值,這時事務A也沒有理由重試整個事務,只需要使用commute在提交的時候,再擷取一次最新的最大值,並對比計算一次然後提交。


使用alter或ref-set,當另外一個事務在當前事務之前提交了改變,當前事務將會會滾重試。commute在滿足順序無關的條件下,能夠提升效能。


在一個事務中,傳遞給commute的參數會被存放在一個有序map裡,map的key是Ref,value是包含了相關函數和參數的list,這個map的順序是根據ref的建立時間來排列的。當事務正在提交的時候,一個寫鎖會按照map的順序依次鎖住該事務涉及的所有Ref,然後對於使用commute函數修改的Ref,會再次調用函數來修改Ref的值。這種按順序鎖定Ref的做法保證了不會發生死結的情況。commute則允許第一次取得的Ref和第二次取得的Ref值可以不同。


在一個事務中,一個已經被commute調用了的Ref是無法被alter或者ref-set修改的,因為被commute調用的Ref意味著在提交階段會被commute再次修改,而alter在commute之後擷取並修改這個Ref的話,提交階段STM無從判定是否應該會滾。(這段還不太理解,需要回頭再看)


有時候需要防止其它事務修改一個當前事務會去讀取或修改的Ref,也叫寫入偏差,可以用ensure函數來解決寫入偏差。ensure能保證其它事務無法修改Ref,但並不保證本事務可以修改Ref,因為其它事務可能也同時用ensure阻止了當前事務修改這個Ref。


在深入ClojureSTM之前,得先理解Validators和Watchers。Validators是一種函數,只要可變類型的變數被修改,就會調用這個函數,如果這個函數返回false或者是拋出異常,那麼表示這次修改是無效的。每種可變類型的變數都唯一對應一個validator校正函數,使用set-validator!函數可以為一種可變類型的變數指定校正函數。有2種途徑可以得知可變類型變數被修改:watch觀測函數和watcher代理。watch觀測函數必須接收4個參數,一個是唯一的key,一個是可變類型變數,還有舊值和新值。key可以用來表明watch函數的目的,也可以任何其它資料。每一個key對應一個變數,同時對應一個watch函數。我們可以使用add-watch函數來為變數指定觀測函數。add-watch函數接收3個參數,變數、key和watch函數,可以用remove-watch函數來為變數解除綁定觀測函數。remove-watch函數接收2個參數,變數和key。watcher代理能夠在變數被修改的時候,收到一個action,這個action是一個函數,代理會把當前值和變數傳遞給action,注意沒有傳舊值哦。


接下來要從抽象層次來看看Clojure的STM實現,但是只針對Clojure的1.0版本,後續的版本可能會有不一樣的實現方式,所以接下來要討論的東西並不能完全代表Clojure的STM。要知道一點,理解STM內部實現並不是必需的,不理解內部實現也是能夠正確使用它的。但是理解內部實現依然很有用。


1.0版本的ClojureSTM實現混雜著java和clojure源碼,之後一些Clojure版本完全用java實現STM,近期則有不少為加大Clojure代碼佔比的工作在默默的進行著,一旦這些工作完成,我們在這裡討論的東西就過時了。不過不要灰心,STM的實現機制並不會有多大的變化。本人的意願是在Clojure更新STM實現的時候,同步更新本文,協助大家理解內部實現,不用去看晦澀的源碼。


Clojure的STM實現是基於MVCC(multi-version concurrency control和snapshot isolation,也就是說Clojure實現了這兩個抽象概念。這兩個抽象概念的標準定義和clojure實現,主要區別是clojure用的是記憶體而不是資料庫表。以下是對MVCC的定義,小括弧中是標準定義相關的。


MVCC使用時間戳或者是事務性id來實現串列運行,MVCC通過維護一個擁有多個版本的對象(或資料庫),確保一個事務不用等待這個對象。對象的每個版本都包含一個改寫時間戳記,每個事務都包含一個事務時間戳記,當事務在讀取對象的時候,會去抓取在事務時間戳記之前的改寫時間戳記最新的那個版本。如果事務Ti想要改寫一個對象,而事務Tk也想改寫這個對象,Ti的事務時間戳記必需先於Tk的事務時間戳記,Ti才能成功改寫對象。也就是說要一個事務要完成寫入動作,它的事務時間戳記必需是最早的那個。每個對象都有一個讀取時間戳記,假設事務Ti想要修改對象P,如果事務時間戳記先於讀取時間戳記,Ti被拋棄並重試,否則Ti會建立新版本的P,並且設定這個版本的改寫時間戳記為事務時間戳記,設定對象的讀取時間戳記為事務時間戳記,注意ClojureSTM並沒有使用讀取時間戳記。這種實現方式的一個顯著缺點是儲存多個版本對象的成本(儲存在資料庫中),優點則是快速讀取,因為讀取不會被阻塞,適合完成讀取密集型的工作,它還適合用於實現“真隔離快照”,真隔離快照能夠使並行作業以很低的消耗執行或者不完全執行。在隔離快照模式下,事務啟動的時候會擷取快照,就好像這個事務獨享對象(資料庫)一樣,事務運行到提交階段的時候會做判斷,只有該快照沒有被其它事務修改的情況下才能成功提交。


隔離快照的一個缺點是會導致寫入偏差write skew,寫入偏差是指並發事務讀取一組對象,並根據這組對象中的一些對象來修改這組對象中的另一些對象,而這些對象之間是有約束關係的。舉個例子,有個鎮子嚴格限制每個家庭最多隻能擁有3隻寵物,寵物只能是貓或狗,李雷有一隻狗,他的老婆韓梅梅有一隻貓,這時候李雷收養了另一隻狗,韓梅梅收養了另一隻貓,他們倆同時並發進行,注意,事務只能看到其它已經提交成功的事務,事務看不到其它未提及的事務內部資料的。李雷的事務修改的是他們家擁有的狗的數量,這並沒有違反上限3的約束,韓梅梅的事務也一樣,都滿足提交的條件,結果導致他們擁有4隻寵物,因為李雷修改的是狗的數量,韓梅梅修改的是貓的數量,事務提交的時候,是根據其它事務是否修改了本事務要修改的對象來決定的,不管他們夫妻倆提交的先後,總是沒有修改對方的要修改的對象,事務總是會成功提交。clojure提供了ensure函數來防止寫入偏差的情況。


ClojureSTM實現使用鎖和鎖自由策略,事務取得鎖以後,立即釋放鎖,而不是整個事務過程都持有鎖。鎖自由策略用於標記Ref變數是否被事務修改過。用Clojure寫並發代碼,比顯式鎖的那套更加簡單,clojure建立一個事務使用dosync函數,並傳入一組運算式(這組運算式也稱作事務體body),不需要指明哪個Ref可能會被事務修改,但是開發人員還是得分清哪些代碼應該放在dosync內,因為在事務體內讀取或修改的一組Ref變數是擁有一致性狀態的,在事務體外讀取就無法保證一致了,而且clojure事務體外是無法修改Ref變數的。


目前ClojureSTM使用了java的並發類有:
java.util.concurrent.AtomicInteger
java.util.concurrent.AtomicLong
java.util.concurrent.Callable
java.util.concurrent.CountDownLatch
java.util.concurrent.TimeUnit
java.util.concurrent.locks.ReentrantReadWriteLock
使用的clojure類主要有:
clojure.lang.LockingTransaction
clojure.lang.Ref


dosync宏包裹著事務體,dosync宏先調用sync宏,sync宏去調用LockingTransaction的靜態方法runInTransaction。sync宏把事務體作為一個匿名函數傳給runInTransaction。每一個線程都持有一個LockingTransaction對象,這些對象都是存放在一個ThreadLocal變數裡,LockingTransaction對象就是一個事務,建立這個對象就是建立事務,在這個對象裡面調用方法就是在事務裡面調用方法。ThreadLocal能夠保證線程訪問到的是線程自己存取的對象。runInTransaction會先進行判斷,如果當前線程還未持有LockingTransaction對象,那麼就會建立一個,然後在這個事務對象裡面運行sync傳過來的匿名函數。如果當前線程已經運行了一個事務,即已經持有LockingTransaction對象,那麼就會在這個事務對象裡運行你們函數。


事務的狀態有如下5種
RUNNING 運行中
COMMITTING 提交中
RETRY 重試
KILLED 撲街
COMMITTED 提交成功
當處於重試狀態,事務將會嘗試一次重試,但是還沒開始試。如果開始重試了,狀態會變成運行。有2種情況會導致事務撲街:1,在事務中調用abort方法,會設定事務狀態為撲街並拋出AbortException異常,事務中止並且不會重試,目前的版本的clojure並沒有調用abort方法的代碼,2,在事務中調用barge方法,會設定事務狀態為撲街,但允許事務可以重試。


每一個Ref對象都有一個tvals欄位,包含一串這個Ref的曆史提交值,tvals的長度不會變短,只會變長。tvals欄位的長度是由Ref對象的另外2個欄位控制的:minHistory和maxHistory,預設是0和10,不同Ref對象的這2個欄位可以各不相同,用ref-min-history和ref-max-history函數可以進行修改。不要忽視這個tvals長度的重要性,請參考下面Faults部分。每個Ref對象都有一個ReentrantReadWriteLock(可重新進入讀寫鎖)。對於一個Ref對象,可以有任意數量的並發事務持有這個Ref的讀取鎖,而只能有一個事務持有這個Ref的改寫鎖。只有一種情況下一個事務的整個生命週期都會持有讀取鎖,那就是使用ensure修改Ref的時候,這種情況下,一個事務持有讀取鎖直到Ref在這個事務中被修改或者這個事務提交。不會發生一個事務整個生命週期都持有改寫鎖的情況。事務在某些情況下擷取改寫鎖,並隨即釋放,在事務提交的時候再次擷取改寫鎖,並在提交完成後釋放,關於鎖的更具體的資訊,參考下面實現層次ClojureSTM部分的lock欄位相關段落。


調用ref-set或alter修改一個Ref的時候,會獲得這個Ref的一個事務內部值,對於外部事務是不可見的,提交成功後才變成外部可見。調用的同時還會修改Ref的tinfo欄位,該欄位描述了修改過這個Ref的事務的順序以及當前事務狀態等資訊。事務就是通過讀取tinfo來瞭解Ref是否正在被另外一個事務修改。可以把tinfo想象成一張門票,Ref持有門票就能夠進入commit階段,但是一張門票只能進入一個事務的commit階段。關於tinfo更具體的資訊,參考下面關於實現層次ClojureSTM部分的lock欄位相關段落。


每一個LockingTransaction對象都有一個vals欄位,該欄位維護一個包含事務內部值的map,map的key是Ref對象,map的val是Ref對應的值,值的類型是java.lang.Object。如果事務周期內只對Ref讀取,那麼值是從tvals欄位裡擷取,多次讀取的話,效率就會相對較低。事務周期內,第一次修改Ref的時候,新的值會存放在vals欄位裡,事務周期內的後續操作就會從vals存取。


當事務內部,在讀取一個Ref的時候,這個Ref即沒有事務內部值(事務的vals欄位沒有這個Ref的key),在Ref的tvals欄位裡也找不到提交時間比當前事務開始的時間更早的值,那麼就會發生“fault”故障。故障發生的時候,事務就會重試。
假設一個Ref從沒經曆過fault故障,之後也不會經曆fault故障,而且它的minHistory是3,maxHistory是6,那麼tvals的長度就會增長到3以後,保持在3,不會繼續增長。
假設一個Ref從沒經曆過fault故障,之後也不會經曆fault故障,而且它的minHistory是0,那麼tvals的長度就不會超過1。
假設一個Ref已經經曆過fault故障,並且這個Ref的tvals的長度小於maxHistory,這時事務A對該Ref提交了一個修改,那麼這個Ref的tvals就會新增一個節點,tvals的長度就可能處於minHistory和maxHistory之間
假設一個Ref已經經曆過fault故障,並且這個Ref的tvals的長度等於maxHistory,這時事務A對該Ref提交了一個修改,那麼這個Ref的tvals就會新增一個節點,並去掉最早的那個節點,tvals的長度就是maxhistory


barge是用來描述當前事務繼續的啟動並執行情況下,另外一個事務是否應該重試。當一個事務A試圖barge(闖入)另外一個事務B,只有滿足這3個條件才能闖入成功:1,A必需至少已經運行了10毫秒,2,A的事務開始時間必需比B早,也就是說,老的事務優先於新的事務,3,B必需是處於RUNNING運行狀態並在A闖入的時候能夠成功修改成KILLED撲街狀態,也就是說B如果處於提交狀態,B就不會被闖入。


重試是指事務拋棄其對Ref的修改,回到事務體開始的地方,重新執行。有4種情況下會發生重試:
1,當事務體用ref-set或alter修改一個Ref的時候,會去擷取這個Ref的鎖,
a,如果其它事務已經佔用了這個Ref的讀取或改寫鎖,那麼當前事務就擷取不到Ref的改寫鎖。
b,如果當前事務啟動後,已經有其它事務提交了對這個Ref的修改。
c,有另一個事務B正在修改這個Ref,但是還未提交,並且事務B嘗試barge闖入其它事務並且嘗試失敗
2,事務A嘗試讀取Ref的值,但是:
a,另一個事務B已經闖入事務A,導致事務A的狀態不是RUNNING
b,Ref並沒有事務內部值,也沒有比本事務開始時間更早的曆史值,即發生了fault。
3,當事務體用ref-set,alter,commute,ensure修改一個Ref後,另一個事務成功barge闖入當前事務,導致當前事務的狀態變成非RUNNING。
4,當前事務正在提交,但是另外一個事務做了一個事務內修改並且嘗試闖入當前事務,並且失敗
事務不會無限制的重試,在LockingTransaction對象裡,有一個RETRY_LIMIT常量,目前的版本的clojure是設定為1萬,如果重試超過這個數,就會拋出異常。


重試是由一個java.lang.Error的子類RetryEx觸發的,這個RetryEx是定義在LockingTransaction類裡,不使用Exception的子類來觸發的理由是:這樣就不會被使用者catch Exception塊攔截到。重試的代碼中包含一個攔截RetryEx的try塊,攔截處理只是簡單的回到事務開始的地方,這個try塊並沒有攔截其它的東西,所以如果發生其它的異常,就會中斷事務。


在clojureSTM實現中,有不少方法會拋出IllegalStateException,如果在事務內拋出這個異常,就不會重試,下面這些情況會拋出這個異常:
1,當前線程嘗試擷取LockTransactin,但是這個對象並不存在,比如在事務外部調用ref-set、alter、commute或者ensure的情況
2,嘗試擷取Ref,但是擷取不到值,比如Ref還沒初始化
3,在事務內部嘗試使用ref-set或alter修改一個已經被commute修改過的Ref
4,Ref的validation函數返回false或拋出異常
在ClojureSTM中不會發生死結deadlock、活鎖livelock、競爭條件race condition。

理解Clojure STM 軟體事務性記憶體

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.