讀古今文學網 > Java程序員修煉之道 > 9.6 actor介紹 >

9.6 actor介紹

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)。