軟體事務記憶體導論(七)阻塞事務
宣告:本文是《Java虛擬機器併發程式設計》的第六章,感謝華章出版社授權併發程式設計網站釋出此文,禁止以任何形式轉載此文。
阻塞事務——有意識地等待
我們經常會遇到這樣一種情況,即某事務T能否成功完成依賴於某個變數是否發生了變化,並且由於這種原因所引起的事務執行失敗也可能只是暫時性的。作為對這種暫時性失敗的響應,我們可能會返回一個錯誤碼並告訴事務T等待一段時間之後再重試。然而在事務T等待期間,即使其他任務已經更改了事務T所依賴的資料,事務T也沒法立即感知到並重試了。為了解決這一問題,Akka為我們提供了一個簡單的工具——retry(),該函式可以先將事務進行回滾,並將事務置為阻塞狀態直到該事物所依賴的引用物件發生變化或事務阻塞的時間超過了之前配置的阻塞超時為止。我本人更願意將這一過程稱為“有意識地等待”,因為這種說法聽起來比“阻塞”更合適一些。下面讓我們將阻塞(或有意識地等待)用於下面的兩個例子當中。
在Java中阻塞事務
程式設計師一般都會對咖啡因上癮,所以加班的時候任何主動要去拿些咖啡回來喝的人都知道不能空手而歸。但是這個拿咖啡的人很聰明,他沒有忙等(busy wait)至咖啡罐被重新填滿,而是在Akka的幫助下給自己設定了一個訊息提醒,一旦咖啡罐有變化他就能收到這個通知。下面讓我們用retry()來實現這個可以有意識等待的fillCup()函式。
public class CoffeePot { private static final long start = System.nanoTime(); private static final Ref<Integer> cups = new Ref<Integer>(24); private static void fillCup(final int numberOfCups) { final TransactionFactory factory = new TransactionFactoryBuilder() .setBlockingAllowed(true) .setTimeout(new DurationInt(6).seconds()) .build(); new Atomic<Object>(factory) { public Object atomically() { if(cups.get() < numberOfCups) { System.out.println("retry........ at " + (System.nanoTime() - start)/1.0e9); retry(); } cups.swap(cups.get() - numberOfCups); System.out.println("filled up...." + numberOfCups); System.out.println("........ at " + (System.nanoTime() - start)/1.0e9); return null; } }.execute(); }
在fillCup()函式中,我們將事務配置成blockingAllowed,並將事務完成的超時時間設為6秒。當發現當前沒有足夠數量的咖啡時,fillCups()函式沒有簡單地返回一個錯誤碼,而是呼叫了StmUtil的retry()函式進行有意識地等待。這將使得當前事務進入阻塞狀態,直到與之相關的cups引用發生變化為止。一旦有任何相關的引用發生改變,系統將啟動一個新事務將之前包含retry的原子性程式碼進行重做。
下面讓我們通過呼叫fillCup()函式來觀察retry()的實際效果:
public static void main(final String[] args) { final Timer timer = new Timer(true); timer.schedule(new TimerTask() { public void run() { System.out.println("Refilling.... at " + (System.nanoTime() - start)/1.0e9); cups.swap(24); } }, 5000); fillCup(20); fillCup(10); try { fillCup(22); } catch(Exception ex) { System.out.println("Failed: " + ex.getMessage()); } } }
在main()函式中,我們啟動了一個每隔大約5秒就往咖啡壺重新裝填咖啡的定時任務。隨後,第一個跑去拿咖啡的同事A立即取走了20杯咖啡。緊接著,當我們這邊自告奮勇去取咖啡的同事B想再取走10杯咖啡時,他的動作將被阻塞直至重新裝填任務完成為止,而這種等待要比不斷重試的方案高效得多。重新裝填的事務完成之後,同事B的請求將被自動重試,而這一次他的請求成功完成了。如果重新裝填任務沒有在我們設定的超時時間內發生,則請求咖啡的事務將會失敗,在上例的try程式碼塊中的那個請求就屬於這種情況。我們可以通過輸出日誌來觀察到這一行為,同時也可以更深入地體會到retry()為我們帶來的便利:
filled up....20 ........ at 0.423589 retry........ at 0.425385 retry........ at 0.427569 Refilling.... at 5.130381 filled up....10 ........ at 5.131149 retry........ at 5.131357 retry........ at 5.131521 Failed: Transaction DefaultTransaction has timed with a total timeout of 6000000000 ns
從上述輸出結果中我們可以看到,第一個倒20杯咖啡的請求是在程式開始執行之後0.4秒左右完成的。而第一個請求完成之後,咖啡壺裡就僅剩餘4杯咖啡了,所以第二個倒10杯咖啡的請求就只能被阻塞,直到程式執行至5秒左右時重新裝填的任務完成為止。在重新裝填的任務完成之後,倒10杯咖啡的事務被重新啟動,並在程式執行到5秒多一點的時候成功完成。最後一個倒22杯咖啡的任務則由於在規定的超時時間內沒有再次發生重新裝填而以失敗告終。
其實我們在日常工作中並不會經常用到retry(),只有當程式邏輯需要執行某些操作、而這些操作又依賴於某些相關資料發生變化的情況下,我們才能受益於這個監控資料變化的特性。
在Scala中阻塞事務
在上面Java版的示例中,我們使用了一個提供了很多STM相關的便利介面的StmUtils物件。而在Scala中,我們可以直接使用StmUtils裡提供的各種特性(trait)。此外,我們在Scala中同樣可以用工廠方法來建立TransactionFactory。
object CoffeePot { val start = System.nanoTime() val cups = Ref(24) def fillCup(numberOfCups : Int) = { val factory = TransactionFactory(blockingAllowed = true, timeout = 6 seconds) atomic(factory) { if(cups.get() < numberOfCups) { println("retry........ at " + (System.nanoTime() - start)/1.0e9) retry() } cups.swap(cups.get() - numberOfCups) println("filled up...." + numberOfCups) println("........ at " + (System.nanoTime() - start)/1.0e9) } } def main(args : Array[String]) : Unit = { val timer = new Timer(true) timer.schedule(new TimerTask() { def run() { println("Refilling.... at " + (System.nanoTime() - start)/1.0e9) cups.swap(24) } }, 5000) fillCup(20) fillCup(10) try { fillCup(22) } catch { case ex => println("Failed: " + ex.getMessage()) } } }
在建立TransactionFactory物件時,我們並沒有直接使用DurationInt來配置事務的超時時間,而是用intToDurationInt()函式來完成從int到DurationInt的隱式轉換。通過Scala隱式轉換所帶來的語法上的便利,我們在初始化TransactionFactory物件時只需簡單呼叫6 seconds即可。示例程式碼中餘下的部分就只是從Java到Scala的一個簡單的翻譯而已,其最終的結果輸出如下所示:
filled up....20 ........ at 0.325964 retry........ at 0.327425 retry........ at 0.329587 Refilling.... at 5.105191 filled up....10 ........ at 5.106074 retry........ at 5.106296 retry........ at 5.106466 Failed: Transaction DefaultTransaction has timed with a total timeout of 6000000000 ns