轉:http://blog.zhaojie.me/2009/05/a-simple-actor-model-implementation.html
Actor模型
Actor模型為並行而生,具Wikipedia中的描述,它原本是為大量獨立的微型處理器所構建的高性能網絡而設計的模型。而目前,單台機器也有了多個獨立的計算單元,這就是為什么在並行程序愈演愈烈的今天,Actor模型又重新回到了人們的視線之中了。Actor模型的理念非常簡單:天下萬物皆為Actor,Actor之間通過發送消息進行通信。Actor模型的執行方式有兩個特點:
- 每個Actor,單線程地依次執行發送給它的消息。
- 不同的Actor可以同時執行它們的消息。
對於第1點至今還有一些爭論,例如Actor是否可以並行執行它的消息,Actor是否應該保證執行順序與消息到達的一致(祥見Wikipedia的相關詞條)。而第2點是毋庸置疑的,因此Actor模型天生就帶有強大的並發特性。我們知道,系統中執行任務的最小單元是線程,數量一定程度上是有限的,而過多的線程會占用大量資源,也無法帶來最好的運行效率,因此真正在同時運行的Actor就會少很多。不過,這並不影響我們從概念上去理解“同一時刻可能有成千上萬個Actor正在運行”這個觀點。在這里,“正在運行”的含義是“處於運行狀態”。
Actor模型的使用無處不在,即使有些地方並沒有明確說采用的Actor模型:
- Google提出的Map/Reduce分布式運算平台
- C#,Java等語言中的lock互斥實現
- 傳統Email信箱的實現
- ……
Actor模型的現有實現
提到Actor模型的實現就不得不提Erlang。Erlang專以Actor模型為准則進行設計,它的每個Actor被稱作是“進程(Process)”,而進程之間唯一的通信方式便是相互發送消息。一個進程要做的,其實只是以下三件事情:
- 創建其他進程
- 向其他進程發送消息
- 接受並處理消息
例如《Programming Erlang》中的一段代碼:
loop() -> receive {From, {store, Key, Value}} -> put(Key, {ok, Value}), From ! {kvs, true}, loop(); {From, {lookup, Key}} -> From ! {kvs, get(Key)}, loop() end.
在Erlang中,大寫開頭的標識表示“變量(variable)”,而小寫開頭的標識表示“原子(atom)”,而大括號及其內部以逗號分割的數據結構,則被稱作是“元組(tuple)”。以上代碼的作用為一個簡單的“名字服務(naming service)”,當接受到{From, {store, Key, Value}}的消息時,則表示從From這個進程發來一個store請求,要求把Value與Key進行映射。而接受到{From, {lookup, Key}}消息時,則表示從From這個進程發來一個請求,要求返回Key所對應的內容。服務本身,也是通過向消息來源進程(即From)發送消息來進行回復的。
從Erlang語言的設計並不復雜,其類型系統更加幾乎可以用“簡陋”來形容,這使得其抽象能力十分欠缺,唯一的復雜數據結構似乎只有“元組”一種而已——不過我們現在不談其缺陷,談其“優勢”。Erlang語言設計的最大特點便是引入了“模式匹配(pattern matching)”,當且僅當受到的消息匹配了我們預設的結構(例如上面的{XXX, {store, YYY, ZZZ}}),則會進入相應的邏輯片斷。其次便是其尾遞歸的特性,可見上面的代碼中在loop方法的結尾再次調用了loop方法。
如果說Erlang語言專為Actor模型而設計,那么Scala語言(學Java的朋友們都去學Scala吧,那才是發展方向)中內置的Actor類庫則是外部語言Actor模型實現的經典案例了:
class Pong extends Actor { def act() { var pongCount = 0 while (true) { receive { case Ping => if (pongCount % 1000 == 0) Console.println("Pong: ping " + pongCount) sender ! Pong pongCount = pongCount + 1 case Stop => Console.println("Pong: stop") exit() } } } }
Pong類繼承了Actor模型,並覆蓋其act方法。由於沒有Erlang的尾遞歸特性,Scala Actor使用一個while (true)進行不斷的循環。獲取到消息之后,將會使用case語句對消息進行判斷,並執行相應邏輯。Scala的Actor類庫充分利用了Scala的語法特性,讓Actor模型好像是Scala內置功能一樣,非常漂亮。
此外,其他較為著名的Actor模型實現還有Io Language、Jetlang、以及.NET平台下的MS CCR和Retlang。后文中我們還會簡單提到.NET下Actor Model實現,其他內容就需要感興趣的朋友們自行挖掘了。
Actor模型中的任務調度
Actor模型的任務調度方式分為“基於線程(thread-based)的調度”以及“基於事件(event-based)的調度”兩種。
基於線程的調度為每個Actor分配一個線程,在接受一個消息(如在Scala Actor中使用receive)時,如果當前Actor的“郵箱(mail box)”為空,則會阻塞當前線程直到獲得消息為止。基於線程的調度實現起來較為簡單,例如在.NET中可以通過Monitor.Wait/Pulse來輕松實現這樣的生產/消費邏輯。不過基於線程的調度缺點也是非常明顯的,由於線程數量受到操作系統的限制,把線程和Actor捆綁起來勢必影響到系統中可以同時的Actor數量。而線程數量一多也會影響到系統資源占用以及調度,而在某些情況下大部分的Actor會處於空閑狀態,而大量阻塞線程既是系統的負擔,也是資源的浪費。因此基於線程的調度是一個擁有重大缺陷的實現,現有的Actor Model大都不會采取這種方式。
於是另一種Actor模型的任務調度方式便是基於事件的調度。“事件”在這里可以簡單理解為“消息到達”事件,而此時才會為Actor的任務分配線程並執行。很容易理解,我們現在便可以使用少量的線程來執行大量Actor產生的任務,既保證了運算資源的充分占用,也不會讓系統在同時進行的太多任務中“疲憊不堪”,這樣系統便可以得到很好的伸縮性。在Scala Actor中也可以選擇使用“react”而不是“recive”方法來使用基於事件的方式來執行任務。
現有的Actor Model一般都會使用基於事件的調度方式。不過某些實現,如MS CCR、Retlang、Jetlang等類庫還需要客戶指定資源分配方式,顯式地指定Actor與資源池(即線程池)之間的對應關系。而如Erlang或Scala則隱藏了這方面的分配邏輯,由系統整體進行統一管理。前者與后者相比,由於進行了更多的人工干涉,其資源分配可以更加合理,執行效率也會更高——不過其缺點也很明顯:會由此帶來額外的復雜度。