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則隱藏了這方面的分配邏輯,由系統整體 進行統一管理。前者與後者相比,由於進行了更多的人工干涉,其資源分配可以更加合理 ,執行效率也會更高——不過其缺點也很明顯:會由此帶來額外的復雜度。
我們即將實現的簡單Actor Model類庫,也將使用了基於事件的調度方式。同樣為了簡 化資源分配的過程,我們將直接使用.NET自帶的線程池來運行任務。