討喜的隔離可變性(十一)調和類型化角色

杜老師說 2022-01-07 15:22:35 阅读数:163

十一 角色

聲明:本文是《Java虛擬機並發編程》的第五章,感謝華章出版社授權並發編程網站發布此文,禁止以任何形式轉載此文。

正如我們在8.7節中所看到的那樣,類型化角色是吸取了面向對象的程序設計和基於角色的程序設計二者的精華所孕育出來的新編程模型。該編程模型集所有我們耳熟能詳的方法於一身,既可以方便地使用函數調用,又能享受角色所帶來的好處。所以,在一個OO應用程序中,相比起普通的角色,我們可能會更傾向於使用類型化的角色。然而與普通角色相類似的是,類型化角色也是各自獨立運行且不提供彼此間事務協調功能的,下面我們將會使用調和類型化角色(coordinating typed actor)來解决這個問題。

Akka幫助我們簡化了將一個普通的類型化角色轉換成一個調和類型化角色的過程。我們只需簡單地用一個特殊的Coordinated注解對相應的接口函數進行標記即可。為了標明某個函數調用序列運行在一個協調事務中,我們需要將這些函數調用包裹在一個coordinate()函數裏。在進行下一步操作之前,該函數(默認情况下)會等待所有函數都提交或回滾。

這種方案有一個限制,那就是只有void函數才能够用Coordinated注解進行標記。這是因為void函數被翻譯為單向調用,並且這些函數可以參與到事務中。而返回值不為void的函數則會被翻譯成雙向阻塞調用,所以這些函數將無法參與到自由運行的並發事務中。

現在讓我們再次重新實現那個我們曾在8.10節中擺弄過很多次的轉賬示例吧。

在Java中使用調和類型化角色

為了使用類型化角色,我們需要准備一對接口/實現類,所以我們先從Account和AccountService這兩個下面將會用到的接口開始入手:

public interface Account {int getBalance();@Coordinated void deposit(final int amount);@Coordinated void withdraw(final int amount);}public interface AccountService {void transfer(final Account from, final Account to, final int amount);}

在Account接口中,唯一比較特別的部分就是其兩個接口函數都用@Coordinated注解進行了標記。通過這兩個標記,我們聲明了這些函數既可以在其自身的事務中運行,也可以加入到其調用者的事務當中。與此相對的是,由於AccountService的實現類將會自行管理其事務,所以AccountService的接口函數都沒有進行標記。

public class AccountImpl extends TypedActor implements Account {private final Ref<Integer> balance = new Ref<Integer>(0);public int getBalance() { return balance.get(); }public void deposit(final int amount) {if (amount > 0) {balance.swap(balance.get() + amount);System.out.println("Received Deposit request " + amount);}}public void withdraw(final int amount) {System.out.println("Received Withdraw request " + amount);if (amount > 0 && balance.get() >= amount)balance.swap(balance.get() - amount);else {System.out.println("...insufficient funds...");throw new RuntimeException("Insufficient fund");}}}

通過繼承TypedActor,我們將AccountImpl聲明為一個角色。在這裏我們沒有使用簡單的本地字段,而是采用了托管的STM引用(Ref)。此外,雖然我們沒有寫任何針對事務的代碼,但是由於AccountImpl類相關接口函數都是用@Coordinated標記過的,所以該類的所有函數都將運行在一個事務中。在deposit()函數中,如果參數amount的值大於0,則deposit()函數將負責把amount的值累加到其當前餘額中。相反地,如果當前餘額balance的值大於參數amount,則withdraw()函數就會在當前餘額中减去amount的值。否則,withdraw()函數將會拋出一個异常以錶明當前操作及外圍事務執行失敗。如果我們沒有為這些函數指定事務,則它們將會運行在自身的默認事務中。而在轉賬的情境下,我們希望存款和取款操作都運行在同一個事務中,所以接下來我們要寫一個AccountServiceImpl來對這兩個操作進行管理:

public class AccountServiceImplextends TypedActor implements AccountService {public void transfer(final Account from, final Account to, final int amount) {coordinate(true, new Atomically() {public void atomically() {Coordinating Typed Actors • 211to.deposit(amount);from.withdraw(amount);}});}}

在上面的代碼中,transfer()函數保證了存取款操作都將在同一事務中完成。需要在事務中執行的代碼都被包裝在Atomically接口的成員函數atomically()裏。而從akka.transactor.Coordination類中靜態引入的coordinate()函數則將以事務的形式運行atomically()函數中的代碼塊。其第一個參數true的作用是指示coordinate()要等待其事務完成(成功或回滾)之後才能返回。所有事務中的函數調用本質上都是(發送)單向消息,所以coordinate()函數只關心事務是否完成,而不會阻塞式地等待各函數的返回結果。

除了角色對象的創建過程略有不同之外,使用這些類型化角色的代碼與使用普通對象的代碼看起來沒什麼兩樣。在創建對象時,我們沒有直接把對象new出來,而是使用了一個工廠類來負責創建工作,具體代碼如下所示:

public class UseAccountService {public static void main(final String[] args)throws InterruptedException {final Account account1 =TypedActor.newInstance(Account.class, AccountImpl.class);final Account account2 =TypedActor.newInstance(Account.class, AccountImpl.class);final AccountService accountService =TypedActor.newInstance(AccountService.class, AccountServiceImpl.class);account1.deposit(1000);account2.deposit(1000);System.out.println("Account1 balance is " + account1.getBalance());System.out.println("Account2 balance is " + account2.getBalance());System.out.println("Let's transfer $20... should succeed");accountService.transfer(account1, account2, 20);Thread.sleep(1000);System.out.println("Account1 balance is " + account1.getBalance());System.out.println("Account2 balance is " + account2.getBalance());212 • Chapter 8. Favoring Isolated MutabilitySystem.out.println("Let's transfer $2000... should not succeed");accountService.transfer(account1, account2, 2000);Thread.sleep(6000);System.out.println("Account1 balance is " + account1.getBalance());System.out.println("Account2 balance is " + account2.getBalance());Actors.registry().shutdownAll();}}

上述代碼行為與我們之前在8.10節中所使用的示例完全相同。其輸出結果如下所示:

Account1 balance is 1000Received Deposit request 1000Account2 balance is 1000Let's transfer $20... should succeedReceived Deposit request 20Received Withdraw request 20Account1 balance is 980Account2 balance is 1020Let's transfer $2000... should not succeedReceived Deposit request 2000Received Withdraw request 2000...insufficient funds...Account1 balance is 980Account2 balance is 1020

正如我們所期待的那樣,調和類型化角色版本的輸出結果與transactor版本的輸出結果基本相同——最後一個失敗的轉賬事務所產生的所有變更最終都被丟弃。

在Scala中使用調和類型化角色

下面讓我們將上述Java版本的示例代碼翻譯成Scala。在Scala中,我們才采用trait來代替接口,而這也是兩種語言在實現方面的第一個不同點。

trait Account {def getBalance() : [email protected] def deposit(amount : Int) : [email protected] def withdraw(amount : Int) : Unit}trait AccountService {def transfer(from : Account, to : Account, amount : Int) : Unit}

Account trait的實現是從Java版本直譯過來的:

class AccountImpl extends TypedActor with Account {val balance = Ref(0)def getBalance() = balance.get()def deposit(amount : Int) = {if (amount > 0) {balance.swap(balance.get() + amount)println("Received Deposit request " + amount)}}def withdraw(amount : Int) = {println("Received Withdraw request " + amount)if (amount > 0 && balance.get() >= amount)balance.swap(balance.get() - amount)else {println("...insufficient funds...")throw new RuntimeException("Insufficient fund")}}}

同樣地, AccountService trait的實現也從Java代碼直譯過來即可:

class AccountServiceImpl extends TypedActor with AccountService {def transfer(from : Account, to : Account, amount : Int) = {coordinate {to.deposit(amount)from.withdraw(amount)}}}

在Scala版本的示例中,調用定義了事務各個組成部分的coordinate()函數的方式比在Java版裏簡化了很多。默認情况下,coordinate()函數需要等待其事務執行完畢(成功或回滾)之後才能返回。同時,由於事務中所有的函數調用本質上都是(發送)單向消息,所以coordinate()函數只關心事務是否完成,而不會阻塞式地等待各函數的返回結果。此外,我們還可以向其傳遞一個可選參數,如coordinate(wait=false),來告知coordinate()函數不用等待事務完成。

最後,我們還需要一個檢驗上述代碼的測試用例:

object UseAccountService {def main(args : Array[String]) = {val account1 =TypedActor.newInstance(classOf[Account], classOf[AccountImpl])val account2 =TypedActor.newInstance(classOf[Account], classOf[AccountImpl])val accountService =TypedActor.newInstance(classOf[AccountService], classOf[AccountServiceImpl])account1.deposit(1000)account2.deposit(1000)println("Account1 balance is " + account1.getBalance())println("Account2 balance is " + account2.getBalance())println("Let's transfer $20... should succeed")accountService.transfer(account1, account2, 20)Thread.sleep(1000)println("Account1 balance is " + account1.getBalance())println("Account2 balance is " + account2.getBalance())println("Let's transfer $2000... should not succeed")accountService.transfer(account1, account2, 2000)Thread.sleep(6000)println("Account1 balance is " + account1.getBalance())println("Account2 balance is " + account2.getBalance())Actors.registry.shutdownAll}}

正如我們從下面的輸出結果中所看到的那樣,Scala實現版本的行為與Java版本完全相同:

Received Deposit request 1000Received Deposit request 1000Account1 balance is 1000Account2 balance is 1000Let's transfer $20... should succeedReceived Deposit request 20Received Withdraw request 20Account1 balance is 980Coordinating Typed Actors • 215Account2 balance is 1020Let's transfer $2000... should not succeedReceived Deposit request 2000Received Withdraw request 2000...insufficient funds...Account1 balance is 980Account2 balance is 1020

原創文章,轉載請注明: 轉載自並發編程網 – ifeve.com本文鏈接地址: 討喜的隔離可變性(十一)調和類型化角色

FavoriteLoading添加本文到我的收藏
版权声明:本文为[杜老師說]所创,转载请带上原文链接,感谢。 https://gsmany.com/2022/01/202201071522346094.html