Programming in Scala ch30 (Actor和併發) 意譯3

 

30.5        好的actor編程風格(P616

前面已經展示了actor的完整用法,本節展示actor一些好的用法和編程風格,按照這種風格寫的併發程序可以更易糾錯,更少出現死鎖和競用衝突。

l  actor不應阻塞

寫得好的actor處理消息時不阻塞,因爲一旦actor阻塞了,其他actor給該actor發的消息可能就無法處理,如果該actor在處理第一條消息時就阻塞,它就處理不了後續消息了。如果多個actor都因等待其他actor的響應而阻塞了,就可能發生更糟糕的情況——死鎖。

actor不應阻塞,而應該能處理多個消息,這一般需要其他actor的協助。例如,原來要直接調用Thread.sleep(此處用來模擬費時操作)來阻塞當前actor的地方,可以創建一個helperactor,讓helper actorsleep一段時間後發消息回來,對比如下程序(1)、(2):

  import actors._, actors.Actor._

  val time = 1000

 

  // 1原來阻塞的程序

  val mainActor1 = actor {

    loop { react {

        case n: Int => Thread.sleep(time)

                         println(n)

        case s => println(s) } }

  }

  1 to 5 foreach { mainActor1 ! _ } // 5秒鐘後打印完數字

 

  // 2改寫由helper actor去阻塞的程序

  val mainActor2: Actor = actor {

    loop { react {

        case n: Int => actor { Thread.sleep(time); mainActor2 ! "wakeup" }

                        println(n)

        case s => println(s) } }

  }

  1 to 5 foreach { mainActor2 ! _ } // 馬上打印數字; 1秒鐘後打印5wakeup

 

程序(2)中的helper actor雖然是阻塞的,但他不接受任何消息,所以這麼用也沒問題。有了helper actor的幫助,mainActor2就能及時處理到達的消息,而不會象mainActor1那樣阻塞等待。

註釋:react的工作原理

返回類型爲Nothing就表明一個函數不會正常返回,而是以exception異常來中斷,react就是這麼幹的。react的實際實現比較複雜,但可以從概念上認爲它是這麼工作的:

當你調用actorstart方法時,有線程會調用actoract方法,如果act方法中有reactreact就會從actor的信箱中查找能分配到某個case分支去處理的消息(receive也如此處理)。如果發現能分配處理的消息,react就讓該case分支處理消息並拋一個異常;如果沒有發現能分配處理的消息,react就把actor“冷藏”起來直至信箱中有新的消息到來,並拋一個異常。就是說無論有沒有消息處理,react都會最後拋個異常,其外層act會接到這個異常,調用此act的線程catch這個異常,但不做任何處理。

這就是爲何react要想處理一條以上的消息,就必須在case分支中再次調用上層act,或者其他方式(loop)以達到再次調用react的目的。

 

l  actor之間用且僅用消息來通訊

actor模型的關鍵就是用安全可靠的act方法來解決“共享數據-鎖”模型的困難,也就是說,actor讓我們寫多線程程序時只用關注各個獨立的單線程程序,他們之間通過消息來通訊,這種對問題的簡化是基於我們在actor之間僅用消息來通訊。

例如,如果BadActor中有一個GoodActor的引用:

class BadActor(a:GoodActor) extends Actor {...}

那在BadActor中即可以通過該引用來直接調用GoodActor的方法,也可以通過“!”來傳遞消息。如果通過引用,BadActor可以讀取GoodActor實例的私有數據,而這些數據可能正被其他線程改寫值,結果就是:你還是避免不了“共享數據-鎖”模型中的麻煩事,即必須保證BadActor線程讀取GoodActor的私有數據時,GoodActor線程在這塊成爲“共享數據”的操作上加鎖。GoodActor只要有了共享數據,就必須來加鎖防範競用衝突和死鎖,你又得從actor模型退回到“共享數據-鎖”模型(注:actor對消息是順序處理的,本來不用考慮共享數據)。

另一方面,這也並不意味着你永遠只用消息傳遞,儘管把“共享數據-鎖”弄正確很難但也不是不可能。ScalaErlang在對待actor上的差異,就在於使用Scala,同一個程序中你可以選擇組合使用actor和“共享數據-鎖”。

例如,想要多個actor共享一個公共的可變map,一種方案即純粹的actor方案是創建一個actor,管理這個可變map,並定義一組讓其他actor存取該可變map的消息:往該共享map中放入key-value對的消息,從在共享map中由key獲取value的消息,等等,還可以定義發送異步響應給之前發來請求的actor的消息。另一種方案,可以使用java.util.concurrt包中已經定義好的ConcurrentHashMap,讓所有actor直接使用該共享map

儘管用actor實現共享map比自己從頭實現ConcurrentHashMap要簡單安全得多,但實際上ConcurrentHashMap已經存在了,而且庫代碼質量也不錯,很容易判斷直接使用ConcurrentHashMap更容易、風險更小。當然,ConcurrentHashMap實現的是同步的共享map,你可以用actor實現異步的共享mapScalaactor庫給你這種選擇權。

註釋:如果考慮使用“共享數據-鎖”

當考慮是否組合使用actor和“共享數據-鎖”模型時,下面的電影(1971年的警匪片“骯髒的哈里”)臺詞獲取能提供點幫助:(亂譯的,沒看懂)

我知道你在想:“他到底會開6槍還是5槍?”老實告訴你,我興奮得有點失控了。但這是隻世界上最牛的手槍,能打爆你的腦袋,你應該問自己:“我是否夠幸運?”來吧,小子。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章