看Erlang中Actor模型的執行方式和優劣
Actor模型為并行而生。由于現在單臺機器中獨立的計算單元也越來越多,Actor模型的重要性也越來越大。Actor模型的理念非常簡單:天下萬物皆為Actor,Actor之間通過發送消息進行通信。不同的Actor可以同時處理各自的消息,從而獲得了大規模的并發能力。
Erlang基于Actor模型實現,我們甚至可以這樣認為,沒有Erlang在業界豎立的豐碑,Actor模型便不會如此受人關注。目前,幾乎所有的主流開發平臺上都有了Actor模型的實現,如Java平臺下的Jetlang以及.NET平臺下的MS CCR和Retlang;還有一些Actor框架專為特定語言設計,如F#的MailboxProcessor以及Scala的Actor類庫;甚至微軟還基于MS CCR構建了一門新的語言Axum。
不過對于.NET平臺下的開發人員來說,我們最常用的語言是C#。無論您是在使用MS CCR還是Retlang(亦或是我寫的ActorLite),在消息的執行階段總是略顯尷尬。本文的目的便是提出一種適合C# Actor的消息執行方式,而這種執行方式還會成為我以后公開的C#中“模式匹配”的基礎。
Erlang中的執行方式
本文將分為三個部分,您目前正在閱讀的***部分,將會觀察Erlang是如何執行消息的。有對比才會有差距,也正是由于Erlang在Actor模型上的示范作用,我們才會意識到C# Actor在使用上有多么的不方便。
作為示例,我們還是使用最經典的乒乓測試。乒乓測試的效果很簡單:ping和pong為兩個Actor對象,首先由ping向pong發送一個“Ping”消息,pong在接受到Ping消息之后,將會向ping發送一個“Pong”消息。在雙方“乒來乓去”幾個回合后,ping將會向pong發起“Finished”,從而停止交互。
乒乓測試的Erlang的實現代碼如下:
- -module(tut15).
- -export([start/0, ping/2, pong/0]).
- ping(0, Pong_PID) ->
- Pong_PID ! finished,
- io:format("ping finished~n", []);
- ping(N, Pong_PID) ->
- Pong_PID ! {ping, self()},
- receive
- pong ->
- io:format("Ping received pong~n", [])
- end,
- ping(N - 1, Pong_PID).
- pong() ->
- receive
- finished ->
- io:format("Pong finished~n", []);
- {ping, Ping_PID} ->
- io:format("Pong received ping~n", []),
- Ping_PID ! pong,
- pong()
- end.
- start() ->
- Pong_PID = spawn(tut15, pong, []),
- spawn(tut15, ping, [3, Pong_PID]).
由于Erlang的函數式編程,尾遞歸,receive原語等特殊的語言特性,其乒乓測試的實現或語義上可能和其他語言有一定區別。不過我們現在還是關注Erlang在消息執行時的特性:模式匹配。
雖然Erlang有諸多優秀特性,但是它的數據抽象能力非常有限。在Erlang中常用的數據結構只有三種:
原子(atom):原子使用小寫開頭的標識符來表示。您可以把原子認為是一種字符串常量來看待,事實上它除了作為標識之外也沒有額外的作用。
綁定(binding):大寫開頭的標示符則為綁定,您可以近似地將其理解為“只能設置一次”的變量。一個綁定內部可以保存任何數據,如一個進程(Erlang的概念,并非指操作系統進程)的id,一個數字,或一個字符串。
元組(tuple):顧名思義,“元組”即為“單元的組合”,單元即為“原子”,“綁定”以及其他“元組”,通過某種方式結合起來。如上述代碼中{ping, Ping_PID}便是一個由原子“ping”和綁定“Ping_PID”組成。當然您也可以寫成{do, {ping, Hello, World}, 7}這種嵌套的元組結構。
Erlang中的receive原語的作用是接受下一條消息,直到有可用消息時它才會執行下面的代碼。Erlang使用了模式匹配(Pattern Matching)來表現接受到不同消息時的邏輯分支。如pong的實現:
- pong() ->
- receive
- finished ->
- io:format("Pong finished~n", []);
- {ping, Ping_PID} ->
- io:format("Pong received ping~n", []),
- Ping_PID ! pong,
- pong()
- end.
在這段代碼中,receive將會設法將消息與兩種模式進行匹配:
原子finished,表示測試結束。
元組{ping, Ping_PID},表示一個元組,其中有兩個單元,首先是ping原子,其次是Ping_PID綁定。
在成功匹配了某個模式之后,其中的綁定也會隨之被賦上特定的值。如匹配了{ping, Ping_PID}之后,Ping_PID便被賦值為ping這個Actor對象的標識符。而在接下來的邏輯中,便可以使用這些“綁定”中的值。由于元組的結構不會受到任何限制,因此開發人員可以使用它來表示任意的抽象數據類型——更確切地說,應該是“數據結構”吧。
Erlang的優勢與缺陷
Erlang在消息執行方式上的優勢在于靈活。Erlang是弱類型語言,在實現的時候可以任意調整消息的內容,或是模式的要求。在Erlang進行模式匹配時往往有種約定:使用“原子”來表示“做什么”,而使用“綁定”來獲取操作所需要的“數據”,這種方式避免了冗余的cast和賦值,在使用的時候頗為靈活。然而,世上沒有***的事物,Erlang的消息執行方式也有缺陷,而且是較為明顯的缺陷。
首先,Erlang的數據抽象能力實在太弱。如果編寫一個略顯復雜的應用程序,您會發現程序里充斥著復雜的元組。您可能會疲于應對那些擁有7、8個單元(甚至跟多)的元組,一個一個數過來到底某個綁定匹配的是第幾項,它的含義究竟是什么——一旦搞錯,程序便會出錯,而且想要調試都較為困難。因此,也有人戲稱Erlang是一門“天生會損害人視力的語言”(令人驚訝的是,那篇文章居然搜不到了,我們只能從搜索引擎上看出點痕跡了)。
而我認為,這并不是Erlang語言中***的問題,Erlang中***的問題也是其“弱類型”特性。例如,現在有一個公用的Service Locator服務,任意類型的Actor都會像SL發送一個消息用于請求某個Service的位置,SL會在得到請求之后,向請求方發送一條消息表示應答。試想,如果SL的功能需要有所修改,作為回復的消息結構產生了變化,那么我們勢必要修改每一個請求方中所匹配的模式。由于消息的發送方和接受方在實際上完全分離,沒有基于任何協議,因此靜態檢查幾乎無從做起。一旦遇到這種需要大規模的修改的情況,Erlang程序便很容易產生差錯。因為一旦有所遺漏,系統便無法正常執行下去了。
您對Erlang的感覺如何?這是一門會影響您編程思維的語言。老趙建議,即使您平時不會使用Erlang,也不妨簡單接觸一下這門語言。它的并發或容災等特性給了我許多啟示。相信您會有不少收獲。
【編輯推薦】