在上一篇文章中,我們構建了一個基於 C# 的簡易 Actor 模型。雖然繼承 Actor<T> 並實作 Receive 方法即可建立 Actor,但 C# 的強型別系統在表達「訊息結構」時顯得笨拙。Erlang 以動態元組與原子輕鬆描述訊息語意,C# 卻需在 object[]、object 型別檢查、或枚舉加參數集合之間兩難。本文將示範 F# 如何以判別聯集、元組與強大的模式匹配,優雅地化解這一痛點,並透過一個聊天室與網頁爬蟲範例,展示 Actor 模型在 F# 下的表達力與並發優勢。
Erlang 的訊息完全動態,開發者可隨意發送、接收任意型別,靈活度極高。C# 雖擁有更強的型別系統,但缺乏「動態性」,導致訊息定義僵化。以簡單聊天程式為例,Erlang 僅需模式匹配不同元組即可表達 start、greeting、say、bye 等指令。C# 若要達到同樣效果,通常有三種妥協做法:
object[] 並手動檢查元素,極其繁瑣;object 為訊息型別,再透過型別判斷分派,需「先定義後使用」,擴充性差;這正是老趙正在努力攻克的難題,也促使我們轉向 F# 尋找突破口。
F# 雖為強型別編譯語言,但其型別系統遠比 C# 靈活。針對聊天案例,我們可定義如下訊息型別:
type Message = string
type ChatMsg =
| Start of Person
| Greeting of Person * Message
| Say of Person * Message
| Bye of Person * Message
此處運用了三大特性:
type Message = string 賦予語意,且為全域有效,優於 C# 的 using 別名。ChatMsg 以多個判別子(Start、Greeting…)封裝不同資料結構,每個判別子可攜帶不同型別參數。Person * Message 以順序組合現有型別,輕鬆構建複合資料。定義完訊息型別,Person Actor 的 Receive 方法即可透過 match 對不同判別子分派處理:
and Person(name: string) =
inherit ChatMsg Actor()
let GetRandom =
let r = new Random(DateTime.Now.Millisecond)
fun () -> r.NextDouble()
member self.Name = name
override self.Receive(message) =
match message with
// 系統要求發起聊天
| Start(p) ->
Console.WriteLine("系統讓{0}向{1}打招呼", self.Name, p.Name)
Greeting(self, "Hi, 有空不?") |> p.Post
// 打招呼
| Greeting(p, msg) ->
Console.WriteLine("{0}向{1}打招呼:{2}", p.Name, self.Name, msg)
if GetRandom() < 0.8 then
Say(self, "好,聊聊。") |> p.Post
else
Bye(self, "沒空,bye!") |> p.Post
// 進行聊天
| Say(p, msg) ->
Console.WriteLine("{0}向{1}說道:{2}", p.Name, self.Name, msg)
if GetRandom() < 0.8 then
Say(self, "繼續聊。") |> p.Post
else
Bye(self, "聊不動了,bye!") |> p.Post
// 結束
| Bye(p, msg) ->
Console.WriteLine("{0}向{1}再見:{2}", p.Name, self.Name, msg)
注意管線運算子 |> 的運用:x |> f 相當於 f(x),將巢狀呼叫轉為線性「訊息發送」風格,極貼合 Actor 模型的直覺。
let startChat() =
let p1 = new Person("Tom")
let p2 = new Person("Jerry")
let p3 = new Person("老趙")
Start(p2) |> p1.Post
Start(p3) |> p2.Post
startChat()
執行結果將根據隨機機率呈現不同對話流程,完美展示非同步、無共享狀態的並發互動。
更貼近實務的例子是多 Actor 協作抓取網頁。Crawler Actor 僅負責下載並回傳結果;monitor 以單例模式接收 URL、分派任務、收集結果:
type Crawler() =
inherit ((obj Actor) * string) Actor()
override self.Receive(message) =
let (monitor, url) = message
let content = (new WebClient()).DownloadString(url)
(url, content) |> monitor.Post
let monitor =
{ new obj Actor() with
override self.Receive(message) =
match message with
| :? string as url -> (self, url) |> (new Crawler()).Post
| :? (string * string) as p ->
let (url, content) = p
Console.WriteLine("{0} => {1}", url, content.Length)
| _ -> failwith "Unrecognized message" }
let urls = [
"https://www.live.com"
"https://www.baidu.com"
"https://www.google.com"
"https://www.cnblogs.com"
"https://www.microsoft.com" ]
List.iter monitor.Post urls
執行結果顯示各網站 HTML 長度,證明多 Actor 並行抓取運作正常。
此 Actor 模型唯一的鎖在於訊息佇列存取,若換成無鎖佇列即可達成完全無鎖實現。然而調度頻率過高(每訊息一次調度)可能成為瓶頸。優化策略包括:
Dispatcher.Execute 每次取出 n 則訊息或超過時間閾值再調度;來源:https://www.cnblogs.com/JeffreyZhao/archive/2009/05/16/a-simple-actor-model-implementation-3.html