Java的顯式鎖和同步模型刻下了歲月的痕跡。在最初設計Java語言時,它是一個奇妙的創新,但也埋下了禍根。Java並發模型本質上是面對兩難境地時採取折中策略的產物。
鎖太少,會導致並發代碼不安全,出現競態條件。鎖太多,系統會喪失活力,代碼癱瘓,工作毫無進展。這就是我們在第4章討論過的,安全性與系統活力之間的矛盾。
使用基於鎖的模型,必須照顧到給定時間內所有可能發生的並發操作。但隨著程序變得越來越大,要做到滴水不漏會變得越來越困難。儘管Java有辦法緩解一些問題,但核心問題還在,如果Java語言不能發佈一個拒絕向後兼容的版本,就不可能從根本上解決這個問題。
非Java語言有機會從頭開始。備選語言可以不向程序員暴露鎖和線程的底層細節,而是在自己的運行時環境中提供額外的並發支持。
這應該沒什麼好奇怪的。畢竟在Java剛剛出現時,Java內存模型就受到過質疑。當時很多C和C++開發人員都對這種想法感到詫異,怎麼能由運行時負責管理內存,而讓開發人員遠離這些細節呢?
我們來看一下Scala基於actor技術的並發模型,看它如何讓並發編程變了樣(也更簡單)。
9.6.1 代碼大舞台
actor是擴展scala.actors.Actor
,並實現了act
方法的對象。希望這個定義能跟你腦海中對Java線程的定義相呼應。它們最大的差別就是actor在大多數情況下都不會通過共享的數據進行溝通。
程序員在共享數據時必須採用最佳實踐。如果你想在actor間共享狀態,Scala不會阻止你。我們只是認為這麼做不好。actor有溝通的渠道:mailbox,從另一個上下文中發送過來的消息(工作項)可以放在mailbox中交給actor,請參見圖9-5。
圖9-5 scala的actor和mailbox
要創建actor,擴展Actor
類就行:
import scala.actors._
class MyActor extends Actor {
def act {
...
}
}
這看起來跟Java代碼中聲明Thread
的子類很像。跟線程一樣,我們也要告訴actor開始啟動,並進入消息接收的狀態,這要調用start
方法。
Scala同樣提供了創建actor的工廠方法actor
(與Java裡創建Runnable
匿名實現類的靜態工廠方法相對應)。用它寫出來的Scala代碼很精煉:
val myactor = actor {
...
}
傳給actor
的代碼塊會變成act
方法中的內容。另外,這樣創建的actor不需要再單獨調用start
,它會自動啟動。
這是一塊香甜的語法糖,但我們還要介紹Scala並發模型的核心部件mailbox,所以別回味了,現在就去看看吧。
9.6.2 用mailbox跟actor通信
從另一個對像給actor發消息很簡單,只要在actor對像上調用!
方法就行了。
然而在接收端要有代碼處理這些消息,否則它們就會堆在mailbox裡。另外,actor方法體通常需要有個循環,以便能處理所有流入的消息。我們在Scala REPL中實際操練一下:
scala> import scala.actors.Actor._
val myact = actor {
while (true) {
receive {
case incoming => println(\"I got mail: \"+ incoming)
}
}
}
myact: scala.actors.Actor = scala.actors.Actor$$anon$1@a760bb0
scala> myact ! \"Hello!\"
I got mail: Hello!
scala> myact ! \"Goodbye!\"
I got mail: Goodbye!
scala> myact ! 34
I got mail: 34
上面代碼中的receive
方法就是actor對消息的處理。而工廠方法的參數(代碼塊)則是消息處理方法的主體。
注意 總體來說,Scala模型跟我們第4章(代碼清單4-13)討論的處理模式相似,Java處理線程相當於acctor的角色,
LinkedBlockingQueue
相當於Scala中的mailbox。Scala只是以非常直白的方式為這種模式提供了語言和類庫層面的支持,可以大量減少使用這種模式時所要編寫的套路化代碼。
儘管這個例子非常簡單,但也包含了很多使用actor的基礎知識:
在actor方法中要用循環的方式處理接收消息流;
用
receive
方法處理接收到的消息;用一組
case
作為receive
的主體。
最後這點還得繼續討論。這一組case
被稱為偏函數1。之所以要這樣用,是因為Scala中的actor還有一點比Java方便。具體來說就是mailbox是不區分類型的。也就是說你可以向actor發送任何類型的消息,actor可以用 類型化模式和構造器模式接收不同類型的消息。
1 在Scala中,偏函數是指類型為PartialFunction[-A,+B]
的函數。A
是其接受的函數類型,B
是其返回的結果類型。偏函數最大的特點就是它只接受其參數定義域的一個子集,而對於這個子集之外的參數則拋出運行時異常。這與case
語句非常契合,因為我們在使用case
語句時常常是匹配一組具體的模式,最後用「_」來代表剩餘的模式。如果一組case語句沒有涵蓋所有的情況,那麼這組case
語句就可以被看做是一個偏函數。——譯者注
除了這些基礎知識,這裡還有一些使用actor的最佳實踐。編寫代碼應該盡量遵循下面幾條規則:
把傳入消息做成不可變的;
考慮把消息類型做成
case
類;不要在actor內部做阻塞操作,一個也別做。
不是每一個程序都需要遵守所有的最佳實踐,但大多數應用程序應該都能從這些建議中受益。
對於更加複雜的actor,經常有必要控制它的啟動和關閉。關閉actor通常都是用帶有Boolean
條件判斷的循環。如果你喜歡,也可以將actor寫成函數式的風格,這樣傳入的消息就不會影響它的狀態。
Scala對基於actor的並發編程提供的支持還有很多。我們在這裡看到的只是皮毛。如果想全面瞭解,請參閱Nilanjan Raychaudhuri的大作Scala in Action(Manning, 2010)。