ActorLite:一个轻量级Actor模型实现(下)
2009-05-16 17:52 Jeffrey Zhao 阅读(26234) 评论(13) 编辑 收藏 举报在上一篇文章中,我们实现了一个简单的Actor模型。如果要构建一个Actor,便只是简单地继承Actor<T>类型并实现其Receive方法即可。在上次文章的末尾,我们使用C#演示了该Actor模型的使用。不过现在我们将尝试一下F#。
C#使用Actor模型的缺陷
在Erlang中,每个消息都使用模式匹配来限制其“结构”或“格式”,以此表达不同含义。C#类型系统的抽象能力远胜于Erlang,但是Erlang的“动态性”使得开发人员可以在程序中随意发送和接收任何类型,这种“自由”为Erlang带来了灵活。我们的Actor模型中,每个Actor对象都需要一种特定的消息格式,而这种消息格式承担了“表现Actor所有职责”的重任,但是一个Actor的职责是可能由任何数据组合而成。例如一段最简单的“聊天”程序,其Actor表示了一个“人”,用Erlang实现可能就会这么写:
loop() -> receive % 系统要求发起聊天,于是向对方打招呼 {start, Person} -> Person ! {self(), {greeting, "你好")}, loop(); % 有人前来发起聊天,于是向对方说了点什么 {Person, {greeting, Message}} -> Person ! {self(), {say, "..."}}, loop(); % 有人前来说话,于是拜拜 {Person, {say, Message}} -> Person ! {self(), {bye, "..."}}, loop(); ... end.
不同的元组(tuple)配合不同的原子(atom)便表示了一条消息的“含义”,但是使用C#您又该怎样来表现这些“命令”呢?您可能会使用:
- 使用object[]作为消息类型,并检查其元素。
- 使用object作为消息类型,并判断消息的具体类型。
- 使用枚举或字符串代表“命令”,配合一个参数集合。
第1种做法十分麻烦;第2种则需要“先定义,后使用”也颇为不易;而第3种做法,平心而论,如果有一个“分发类库”的支持就会比较理想——可能比这篇文章中的F#还要理想。老赵正在努力实现这一功能,因为C#的这个特性会影响到.NET平台下所有Actor模型(如第一篇文章中所提到的CCR或Retlang)的使用。
而目前,我们先来看看F#是否可以略为缓解一下这方面的问题。
在F#中使用Actor模型
Erlang没有严谨的类型系统,其“消息类型”是完全动态的,因此非常灵活。那么F#又有什么“法宝”可以解决C#中所遇到的尴尬呢?在现在这个问题上,F#有三个领先于C#的关键:
- 灵活的类型系统
- 强大的模式匹配
- 自由的语法
虽然F#也是强类型的编译型语言(这点和C#一致),但是F#的类型系统较C#灵活许多,例如在“聊天”这个示例中,我们就可以编写如下类型作为“消息”类型:
type Message = string type ChatMsg = | Start of Person | Greeting of Person * Message | Say of Person * Message | Bye of Person * Message
在这个定义中用到了F#类型系统中的三个特点:
- 类型别名:即type Message = string。为一个已有的类型定义一个别名,可以得到更好的语义。与C#使用using定义别名不同的是,F#中的别名可以定义为全局性的,而不仅仅是“源代码”级别的别名。
- Discriminated Unions:即type ChatMsg = …。Discriminated Unions可以为一个类型指定多个discriminator,每个discriminator由一个名称,以及另一种具体类型来表示。不同的discriminator的具体类型可以不同。
- 元组(Tuple):即Person * Message。在F#中可以通过把现有类型按顺序进行任意组合来得到新的类型,这种类型便被称为“元组”。
在Actor模型中,我们便组合了F#的三个特别特性,定义了消息的具体类型。而在使用时,我们便可以使用“模式匹配”对不同的“消息”——其实是CharMsg的不同discriminator进行不同地处理。于是具体的Actor类型Person,便可以使用如下定义:
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
Person类的构造函数接受一个name作为参数,并将其放置到Name属性中。我们同时定义了GetRandom函数,它会在内部构造一个System.Random对象,并每次返回NextDouble方法的值(请注意,无论调用多少次GetRandom方法,永远使用了同一个Random对象,因为他是在定义GetRandom方法时创建的)。而在override的Receive方法中,我们使用“模式匹配”对message对象进行处理:
// 系统要求发起聊天 | Start(p) -> Console.WriteLine("系统让{0}向{1}打招呼", self.Name, p.Name) Greeting(self, "Hi, 有空不?") |> p.Post
请注意上述最后一行,原本我们使用p.Post(…)的调用方式,现在使用了“|>”符号代替。在F#中,x |> f便代表了f(x),它的本意是可以把f(g(h(x)))这样冗余的调用方式转变为清晰的“消息发送”形式:x |> h |> g |> f。而“消息发送”也恰好是我们所需要的“感觉”。因此,我们在接下来的代码中也使用这样的方式:
// 打招呼 | 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)
至此,Person类型定义完毕。我们构造三个Person对象,让它们随意聊天:
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()
结果如下(内容会根据随机结果不同而有所改变):
系统让Tom向Jerry打招呼 系统让Jerry向老赵打招呼 Jerry向老赵打招呼:Hi, 有空不? Tom向Jerry打招呼:Hi, 有空不? Jerry向Tom说道:好,聊聊。 老赵向Jerry说道:好,聊聊。 Jerry向老赵说道:继续聊。 Tom向Jerry说道:继续聊。 Jerry向Tom说道:继续聊。 老赵向Jerry说道:继续聊。 Jerry向老赵说道:继续聊。 Tom向Jerry再见:聊不动了,bye! 老赵向Jerry说道:继续聊。 Jerry向老赵再见:聊不动了,bye!
使用Actor模型抓取网络数据
我们再来看一个略为“现实”一点的例子,需要多个Actor进行配合。首先,我们定义一个“抓取”数据用的Actor,它的唯一作用便是接受一个消息,并将抓取结果传回:
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
再使用“单件”方式直接定义一个monitor对象:
let monitor = { new obj Actor() with override self.Receive(message) = match message with // crawling | :? string as url -> (self, url) |> (new Crawler()).Post // get crawled result | :? (string * string) as p -> let (url, content) = p Console.WriteLine("{0} => {1}", url, content.Length) // unrecognized message | _ -> failwith "Unrecognized message" }
每次收到“抓取”消息时,monitor都会创建一个Crawler对象,并把url发送给它,并等待回复消息。而在使用时,只要把对象一个一个“发送”给monitor便可:
let urls = [ "http://www.live.com"; "http://www.baidu.com"; "http://www.google.com"; "http://www.cnblogs.com"; "http://www.microsoft.com"] List.iter monitor.Post urls
运行结果如下:
http://www.live.com => 18035 http://www.google.com => 6942 http://www.cnblogs.com => 62688 http://www.microsoft.com => 1020 http://www.baidu.com => 3402
性能分析
最后,我们再对这个Actor模型的性能作一点简单的分析。
如果从“锁”的角度来说,这个Actor模型唯一的锁是在消息队列的访问上,这基本上就是唯一的瓶颈。如果把它替换为lock-free的队列,那么整个Actor模型就是完全的lock-free实现,其“调度”性能可谓良好。
不过,从另一个角度来说,这个Actor模型的调度非常频繁,每次只执行一个消息。试想,如果执行一个消息只需要50毫秒,而进行一次调度就需要100毫秒,那么这个性能的瓶颈还是落在“调度”上。因此,如果我们需要进一步提高Actor模型的性能,则需要从Dispatcher.Execute方法上做文章,例如把每次执行一个消息修改为每次执行n个消息,或超过一个时间的阈值再进行下一次调度。减少调度,也是提高Actor模型性能的关键之一。
此外,如果觉得.NET自带的线程池性能不高,或者说会受到程序其他部分的影响,那么也可以使用独立的线程池进行替换。
自然,任何性能优化都不能只凭感觉下手,一切都要用数据说话,因此在优化时一定要先建立合适的Profile机制,保证每一步优化都是有效的。