天天看點

讨喜的隔離可變性(三)建立角色

正如前面曾經提到過的那樣,雖然我們有很多支援角色的類庫可供選擇,但是在本書中我們将使用akka。這是一個基于scala的類庫,該類庫擁有非常好的性能和可擴充性、并同時支援角色和stm。此外,該類庫還可以被用于多種jvm上的語言中。在本章中,我們将注意力集中在java和scala身上。而在下一章,我們将會學習如何在其他語言中使用akka的角色。

讨喜的隔離可變性(三)建立角色

圖 8‑2 某個角色的生存周期

由于akka是用scala實作的,是以在scala中建立和使用角色非常簡單并且更加自然,從akka api的實作裡我們也可以看到scala簡約而不簡單的風格閃耀其中。除此之外,akka的開發者們還設計了一套相當出色的傳統java api,可以使我們在java代碼中很友善地建立和使用角色。下面我們将先學習如何在java中使用這套api,然後再體驗一下用scala時将有着怎樣的簡化和改變。

<b>用java</b><b>建立角色</b>

在akka中,抽象類akka.actor.untypedactor用于表示一個角色的抽象表示,而具體的角色定義則隻需簡單繼承這個抽象類并實作其onreceive()函數就可以了——每當有消息到達此角色時該函數将被調用。下面讓我們通過一個簡單的執行個體來對上述過程建立一個直覺感受。下面我們将會建立一個角色(actor)…不如就寫一個可以對扮演不同熒幕人物(role)的請求進行響應的hollywoodactor咋樣?

<code>1</code>

<code>&lt;br /&gt;</code>

<code>2</code>

<code>public</code> <code>class</code> <code>hollywoodactor</code><code>extends</code> <code>untypedactor {&lt;br /&gt;</code>

<code>3</code>

<code>public</code> <code>void</code> <code>onreceive(</code><code>final</code> <code>object role) {&lt;br /&gt;</code>

<code>4</code>

<code>system.out.println(&amp;quot;playing &amp;quot; + role +&lt;br /&gt;</code>

<code>5</code>

<code>&amp;quot; from thread &amp;quot; + thread.currentthread().getname());&lt;br /&gt;</code>

<code>6</code>

<code>}&lt;br /&gt;</code>

<code>7</code>

如上所示,onreceive()函數接受一個object對象作為其參數。在本例中,我們隻是簡單地将該參數以及負責處理消息的線程的詳情列印出來。稍後我們将會學習如何處理不同類型的消息。

在完成了角色(actor)的定義之後,我們還需要建立一個角色的執行個體,并将該角色(actor)曾經演過的熒幕人物(role)以消息的形式發送給它,下面讓我們來實作這部分内容:

<code>01</code>

<code>02</code>

<code>public class usehollywoodactor {&lt;br /&gt;</code>

<code>03</code>

<code>public static void main(final string[] args) throws interruptedexception {&lt;br /&gt;</code>

<code>04</code>

<code>final actorref johnnydepp = actors.actorof(hollywoodactor.class).start();&lt;br /&gt;</code>

<code>05</code>

<code>johnnydepp.sendoneway(&amp;quot;jack sparrow&amp;quot;);&lt;br /&gt;</code>

<code>06</code>

<code>thread.sleep(100);&lt;br /&gt;</code>

<code>07</code>

<code>johnnydepp.sendoneway(&amp;quot;edward scissorhands&amp;quot;);&lt;br /&gt;</code>

<code>08</code>

<code>09</code>

<code>johnnydepp.sendoneway(&amp;quot;willy wonka&amp;quot;);&lt;br /&gt;</code>

<code>10</code>

<code>actors.registry().shutdownall();&lt;br /&gt;</code>

<code>11</code>

<code>12</code>

在java中我們通常都是用new來建立對象的,但由于akka的角色并非簡單對象而是活動對象(active objects),是以我們需要用一個特殊函數actorof()來完成建立動作。此外,我們還可以先用new生成一個執行個體,然後再調用actorof()對該執行個體進行封裝以獲得一個角色的引用,關于這種建立方式我們稍後會再研究具體細節。當我們建立好了角色之後,就可以通過調用其start()函數來啟動該角色。而當我們啟動一個角色時,akka會将其寫入一個系統資料庫(registry)中,于是在這個角色停止運作之前我們都可以通過系統資料庫來通路它。在本例中,johnnydeep即為角色執行個體的引用,其類型為actorref。

接下來,我們通過sendoneway()函數向johnnydeep發送了一些附帶着我們希望其扮演的熒幕人物(role)的消息。當消息發出之後,其實我們本不用加入那幾個100毫秒等待時間的,但插入延時将有助于我們更好地學習角色如何進行線程切換的運作細節。在代碼的結尾處,我們關閉了所有運作中的角色。除了代碼示例中所使用的shutdownall()之外,我們還可以逐個調用每個角色的stop()函數或給所有角色發送kill消息的方式來達到關停所有角色的目的。

為了能夠運作上面的執行個體,我們需要先把akka的庫檔案都添加到classpath中,然後通過javac對代碼進行編譯。編譯完成之後,我們就可以像運作其他正常java程式一樣運作本節的示例程式。需要再次提醒你的是,請務必記得将所有相關的jar都添加到classpath中。下面就是我在我的系統上所使用的編譯和運作指令:

<code>javac -d . -classpath $akka_jars hollywoodactor.java usehollywoodactor.java&lt;br /&gt;</code>

<code>java -classpath $akka_jars com.agiledeveloper.pcj.usehollywoodactor&lt;br /&gt;</code>

其中akka_jars的定義如下所示:

<code>export akka_jars=&amp;quot;$akka_home/lib/scala-library.jar:\&lt;br /&gt;</code>

<code>$akka_home/lib/akka/akka-stm-1.1.3.jar:\&lt;br /&gt;</code>

<code>$akka_home/lib/akka/akka-actor-1.1.3.jar:\&lt;br /&gt;</code>

<code>$akka_home/lib/akka/multiverse-alpha-0.6.2.jar:\&lt;br /&gt;</code>

<code>$akka_home/lib/akka/akka-typed-actor-1.1.3.jar:\&lt;br /&gt;</code>

<code>$akka_home/lib/akka/aspectwerkz-2.2.3.jar:\&lt;br /&gt;</code>

<code>8</code>

<code>$akka_home/config:\&lt;br /&gt;</code>

<code>9</code>

<code>.&amp;quot;&lt;br /&gt;</code>

為了使執行個體代碼能否順利地編譯運作,請根據你所使用的作業系統來定義akka_jars環境變量,以便編譯器能夠正确定位到scala和akka的安裝路徑。其中,scala-library.jar是scala相關的功能集合,而我們既可以使用akka自帶的jar,也可以使用scala安裝路徑下的那一份。

預設情況下akka會将額外的日志消息輸出到控制台,關于如何對這一行為進行配置請參閱6.8節。

下面讓我們編譯并運作示例代碼,并觀察角色對于消息的響應情況:

<code>playing jack sparrow from thread akka:event-driven:dispatcher:global-</code><code>1</code><code>&lt;br /&gt;</code>

<code>playing edward scissorhands from thread akka:event-driven:dispatcher:global-</code><code>2</code><code>&lt;br /&gt;</code>

<code>playing willy wonka from thread akka:event-driven:dispatcher:global-</code><code>3</code><code>&lt;br /&gt;</code>

通過輸出結果我們可以看到,示例角色每次隻響應一個消息,并且每次運作角色的線程都是不同的。對于消息處理的過程而言,既可以一個線程處理多個消息,也可以像本例這樣由不同線程處理不同的消息——但無論是哪種處理模式,在任意時刻都隻能有一個消息被處理。該模式的關鍵點在于,所有角色都是單線程的,但是在陷入等待狀态時角色會優雅地将線程釋放而不是抓住線程不撒手。我們在發送消息之後插入的sleep語句的目的就是為了将actor引入等待狀态以便更清晰地示範這一運作細節。

上例中,我們建立角色時沒有帶任何構造函參。而如果需要的話,我們可以在角色的建立過程中引入一些參數。例如,我們可以用好萊塢演員的名字來初始化之前的hollywoodactor:

<code>final actorref tomhanks = actors.actorof(new untypedactorfactory() {&lt;br /&gt;</code>

<code>public untypedactor create() { return new hollywoodactor(&amp;quot;hanks&amp;quot;); }&lt;br /&gt;</code>

<code>}).start();&lt;br /&gt;</code>

<code>tomhanks.sendoneway(&amp;quot;james lovell&amp;quot;);&lt;br /&gt;</code>

<code>tomhanks.sendoneway(new stringbuilder(&amp;quot;politics&amp;quot;));&lt;br /&gt;</code>

<code>tomhanks.sendoneway(&amp;quot;forrest gump&amp;quot;);&lt;br /&gt;</code>

<code>thread.sleep(1000);&lt;br /&gt;</code>

<code>tomhanks.stop();&lt;br /&gt;</code>

<code>13</code>

新版的hollywoodactor類的構造函數定義了一個名為name的string類型參數。而在onreceive()函數中,我們對于不能識别的消息進行了專門的處理,即簡單地在螢幕輸出該好萊塢演員未曾飾演過那個未識别的消息所代表的熒幕人物(role)。當然我們也可以采取其他動作,比如傳回一個錯誤碼、打日志、向上層調用者抛異常等等。下面讓我們看看如何将給這個構造函數傳遞參數:

<code>public class hollywoodactor extends untypedactor {&lt;br /&gt;</code>

<code>private final string name;&lt;br /&gt;</code>

<code>public hollywoodactor(final string thename) { name = thename; }&lt;br /&gt;</code>

<code>public void onreceive(final object role) {&lt;br /&gt;</code>

<code>if(role instanceof string)&lt;br /&gt;</code>

<code>system.out.println(string.format(&amp;quot;%s playing %s&amp;quot;, name, role));&lt;br /&gt;</code>

<code>else&lt;br /&gt;</code>

<code>system.out.println(name + &amp;quot; plays no &amp;quot; + role);&lt;br /&gt;</code>

一般情況下,我們都是通過發送消息而不是直接調用函數的方式與角色進行互動的。akka不希望我們拿到角色的直接引用,而是希望我們隻針對actorref的引用進行操作。這樣一來,akka就可以確定我們不會往角色裡添加其他函數,并且也不會與角色執行個體進行直接的互動。直接操縱角色執行個體的行為會将我們帶回到共享可變性的泥淖中,而這正是我們極力想要避免。此外,這種受控的角色建立方式也便于akka更好地回收廢棄的角色。是以如果我們試圖直接建立一個角色類的執行個體,akka将抛出一個内容為“請不要用’new’操作符顯示地建立角色執行個體”的akka.actor.actorinitializationexception異常。

akka允許我們以一種受控的方式建立角色執行個體,即我們可以在一個匿名類中實作untypedactorfactory接口,并在其create()函數中實作建立角色執行個體的邏輯。而接下來的actorof()則把一個繼承自untypedactor的普通對象轉換為為一個akka角色。随後,我們和之前一樣向這個actor發送幾條消息并觀察輸出結果。

在本例中,hollywoodactor隻接受string類型的消息,但我們在測試用例中向其發送了一條值為politics、類型為stringbuilder的消息。而我們在onreceive()函數中設計的檢查邏輯将會發現并處理這一情況。最後,我們會調用stop()函數來終止角色的運作。代碼結尾處插入sleep(1000)的目的是為了讓角色在結束之前有機會響應所有未處理的消息。最終的輸出結果如下所示:

<code>hanks playing james lovell&lt;br /&gt;</code>

<code>hanks plays no politics&lt;br /&gt;</code>

<code>hanks playing forrest gump&lt;br /&gt;</code>

<b>用scala</b><b>建立角色</b>

在scala中建立akka角色時,我們沒有像在java版本中那樣繼承untypedactor類,而是要繼承actor trait并實作receive()函數。下面讓我們用scala來實作之前剛剛用java寫過的hollywoodactor類:

<code>class</code> <code>hollywoodactor</code><code>extends</code> <code>actor {&lt;br /&gt;</code>

<code>def</code> <code>receive</code><code>=</code> <code>{&lt;br /&gt;</code>

<code>case</code> <code>role</code><code>=</code><code>&amp;gt;&lt;br /&gt;</code>

<code>println(&amp;quot;playing &amp;quot; + role +&lt;br /&gt;</code>

<code>&amp;quot; from thread &amp;quot; + thread.currentthread().getname())&lt;br /&gt;</code>

在上面的代碼中,receive()函數實作了一個partialfunction并采用了scala模式比對的形式,但為了避免分散注意力我們現在先忽略這些細節。當有消息到達時,receive()函數将被調用;如果對scala文法還不熟悉的話,你可以暫時先把receive()函數想象成一個大的switch語句,其實作的功能與java版本是完全相同的。

至此我們已經看到了如何定義一個角色,下面讓我們把注意力集中到角色的使用上面:

<code>object</code> <code>usehollywoodactor {&lt;br /&gt;</code>

<code>def</code> <code>main(args</code><code>:</code> <code>array[string])</code><code>:</code><code>unit</code><code>=</code> <code>{&lt;br /&gt;</code>

<code>val</code> <code>johnnydepp</code><code>=</code> <code>actor.actorof[hollywoodactor].start()&lt;br /&gt;</code>

<code>johnnydepp ! &amp;quot;jack sparrow&amp;quot;&lt;br /&gt;</code>

<code>thread.sleep(</code><code>100</code><code>)&lt;br /&gt;</code>

<code>johnnydepp ! &amp;quot;edward scissorhands&amp;quot;&lt;br /&gt;</code>

<code>johnnydepp ! &amp;quot;willy wonka&amp;quot;&lt;br /&gt;</code>

<code>actors.registry.shutdownall&lt;br /&gt;</code>

actor類的actorof()函數有多個重載定義,這裡我們所采用的是接受一個角色類名(即代碼中的 [hollywoodactor])作為其參數的版本。在角色被建立出來之後,我們随即通過調用start()函數将其啟動。在本例中,actorref類型的變量johnnydepp即為我們所建立的角色執行個體的引用。由于scala可以進行類型推斷,是以我們可以不必在代碼中明确指定johnnydepp的類型。

接下來,我們給johnnydepp發送了3個附帶着我們希望其扮演的熒幕人物的消息。噢,稍等一下,這裡有一個細節請你注意,即我們是通過特殊函數!來發送消息的。當你見到actor!message時,請從右向左閱讀這個語句,就能明白這條語句的意思是把消息發送給指定的角色。這處細節再次展現了scala在文法方面的簡潔與優雅。通過這種方式,我們就無需再将發送消息的語句寫成actor.!(message),而是簡單地将句點和括号拿掉,簡寫成actor!message就行了。如果我們更喜歡java裡發送消息的那個函數,那麼我們也可以把scala簡潔的文法用在java風格的函數上,即把語句寫成actor sendoneway message。上面示例中餘下的代碼與之前java版本的示例完全相同,這裡就不再贅述。

下面我們将通過scalac編譯器對上述代碼進行編譯,但首先請務必記住要把akka庫檔案添加到classpath中。編譯完成後,我們就可以像之前運作普通java程式那樣運作上面的scala示例程式。需要再次提醒你的是,請務必記得将所需的jars加入到你系統的classpath中。下面是我在我的系統上所使用的編譯和運作指令,請你根據你系統中scala和akka的安裝目錄來自行調整classpath中相關的路徑資訊:

<code>scalac -classpath $akka_jars hollywoodactor.scala usehollywoodactor.scala&lt;br /&gt;</code>

如果我們想要禁止日志消息輸出到控制台的話,請參閱6.8節中的相關内容。在将上述示例代碼編譯并運作之後,我們可以看到其輸出結果與之前的java版本是非常相似的:

<code>class hollywoodactor(val name : string) extends actor {&lt;br /&gt;</code>

<code>def receive = {&lt;br /&gt;</code>

<code>case role : string =&amp;gt; println(string.format(&amp;quot;%s playing %s&amp;quot;, name, role))&lt;br /&gt;</code>

<code>case msg =&amp;gt; println(name + &amp;quot; plays no &amp;quot; + msg)&lt;br /&gt;</code>

如果想在建立角色時傳些參數給它,如好萊塢演員的名字等,你會發現用scala來實作會比之前的java版本簡單很多。下面讓我們先對hollywoodactor類進行改造,以使其可以接受構造函參:

如上所示,新版本的hollywoodactor類接受一個名為name的string類型的構造函參。而在receive()函數中,我們對于格式無法識别的消息做了專門的處理。在scala中我們無需再使用instanceof,receive()函數中的case語句即可實作消息與各種模式之間的比對——在本例中特指消息類型的比對。

我們用java建立接受一個構造函參的角色時還是花了不少力氣的,但在scala中一切變得如此簡單:

<code>object usehollywoodactor {&lt;br /&gt;</code>

<code>def main(args : array[string]) : unit = {&lt;br /&gt;</code>

<code>val tomhanks = actor.actorof(new hollywoodactor(&amp;quot;hanks&amp;quot;)).start()&lt;br /&gt;</code>

<code>tomhanks ! &amp;quot;james lovell&amp;quot;&lt;br /&gt;</code>

<code>tomhanks ! new stringbuilder(&amp;quot;politics&amp;quot;)&lt;br /&gt;</code>

<code>tomhanks ! &amp;quot;forrest gump&amp;quot;&lt;br /&gt;</code>

<code>thread.sleep(1000)&lt;br /&gt;</code>

<code>tomhanks.stop()&lt;br /&gt;</code>

在上面的代碼中,我們先用new關鍵字對角色進行初始化,随後又将執行個體化好的對象傳給actorof()函數(這是由于akka禁止在actorof()函數之外随意地建立actor執行個體)。通過這一動作,我們就将一個繼承自actor的普通對象轉換成了一個akka角色。接下來,我們同樣會給新建立的角色發送3條消息。剩下的代碼與java版本非常相似,這裡就不再贅述。最後讓我們運作上述示例代碼,并确認其輸出與java版本是否相同: