Hello World
Spiga

ActorLite:一个轻量级Actor模型实现(下)

2009-05-16 17:52 by 老赵, 24020 visits

上一篇文章中,我们实现了一个简单的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#您又该怎样来表现这些“命令”呢?您可能会使用:

  1. 使用object[]作为消息类型,并检查其元素。
  2. 使用object作为消息类型,并判断消息的具体类型。
  3. 使用枚举或字符串代表“命令”,配合一个参数集合。

第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机制,保证每一步优化都是有效的。

 

源代码及示例下载:http://code.msdn.microsoft.com/ActorLite

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

12 条回复

  1. DiryBoy
    *.*.*.*
    链接

    DiryBoy 2009-05-16 20:28:00

    沙发? 俺还是学好基础再来研究了..

  2. 技术爱好者
    *.*.*.*
    链接

    技术爱好者 2009-05-17 10:29:00

    越来越复杂了,就上上面仁兄说的一样,学好了基础再说吧

  3. 没注册[未注册用户]
    *.*.*.*
    链接

    没注册[未注册用户] 2009-05-18 00:21:00

    老赵,推荐的书呢?还没有好好的读过一本书,等你推荐呢.怎么没有下文了?

  4. 老赵
    admin
    链接

    老赵 2009-05-18 00:26:00

    @没注册
    文章欠债太多,来不及写啊。而且推荐文章真的很难写。
    // 透露一下,第一本就是随大流的著名的SICP,正在重新扫一遍呢。

  5. 汝熹
    *.*.*.*
    链接

    汝熹 2009-05-19 08:35:00

    感觉Actor Model就是管道线程模式,初研究线程。

  6. 老赵
    admin
    链接

    老赵 2009-05-19 10:01:00

    @汝熹
    略有相似之处。

  7. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-05-19 14:51:00

    上次 .net大会 我问jr 一个很大规模的聊天程序 是在所有的end receive回调里面处理事情 还是接收了数据放到一个队列里面

    他说
    如果你要性能
    那么永远不要放在队列里面

    随便说说

  8. 老赵
    admin
    链接

    老赵 2009-05-19 14:59:00

    @韦恩卑鄙
    性能,并发,伸缩,不是一个概念。
    用消息队列解决的是并发和伸缩问题,要说性能,当然是立马做掉最快。
    但是,对于单个Actor最好的性能,对于全局来说不一定就是最好的。

  9. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-05-19 15:07:00

    所以才很纠结阿

  10. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-05-19 15:09:00

    对了。。。 因为一个印度人因为不喜欢mvc 换了个team 我就调到他的研究岗位了
    好消息是再见了 vb6
    坏消息是 必须马上学习asp.net mvc 还是移动开发。。。

    以后要多问你啦~~~

  11. 老赵
    admin
    链接

    老赵 2009-05-19 15:10:00

    @韦恩卑鄙
    不纠结啊,性能差也不是指差到不能接受,总有办法的。
    还比如,对于单个来说执行实现从0.3s变成了0.5s,超过了50%的性能下降,但是对单个用户来说还是差不多。整个系统吞吐量上去了,就ok了。

  12. 老赵
    admin
    链接

    老赵 2009-05-19 15:11:00

    @韦恩卑鄙
    我不懂移动开发,不懂前端,我只懂框架怎么用,但是要我css,html,我就不行了……

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我