Hello World
Spiga

适合C# Actor的消息执行方式(2):C# Actor的尴尬

2009-07-13 00:24 by 老赵, 12137 visits

上一篇文章中,我们简单解读了Erlang在执行消息时候的方式。而现在,我们就一起来看看,C# Actor究竟出现了什么样的尴尬。此外,我还打算用F#进行补充说明,最终我们会发现,虽然F#看上去很美,但是在实际使用过程中依旧有些遗憾。

Erlang中的Tag Message

老赵在上一篇文章里提到,Erlang中有一个“约定俗成”,使用“原子(atom)”来表示这条消息“做什么”,并使用“绑定(binding)”来获取做事情所需要的“参数”。Erlang大拿,《Programming Erlang》一书的主要译者jackyz同学看了老赵的文章后指出,这一点在Erlang编程规范中有着明确的说法,是为“Tag Message”:

5.7 Tag messages

All messages should be tagged. This makes the order in the receive statement less important and the implementation of new messages easier.

Don’t program like this:

loop(State) ->
 
receive
    ...
    {
Mod, Funcs, Args} -> % Don't do this
     
apply(Mod, Funcs, Args},
     
loop(State);
    ...
 
end.

The new message {get_status_info, From, Option} will introduce a conflict if it is placed below the {Mod, Func, Args} message.

If messages are synchronous, the return message should be tagged with a new atom, describing the returned message. Example: if the incoming message is tagged get_status_info, the returned message could be tagged status_info. One reason for choosing different tags is to make debugging easier.

This is a good solution:

loop(State) ->
 
receive
    ...
    {
execute, Mod, Funcs, Args} -> % Use a tagged message.
     
apply(Mod, Funcs, Args},
     
loop(State);
    {
get_status_info, From, Option} ->
     
From ! {status_info, get_status_info(Option, State)},
     
loop(State);   
    ...
 
end.

第一段代码使用的模式为拥有三个“绑定”的“元组”。由于Erlang的弱类型特性,任何拥有三个元素的元组都会被匹配到,这不是一个优秀的实践。在第二个示例中,每个模式使用一个“原子”来进行约束,这样可以获取到相对具体的消息。为什么说“相对”?还是因为Erlang的弱类型特性,Erlang无法对From和Option提出更多的描述。同样它也无法得知execute或get_status_info这两个tag的来源——当然,在许多时候,它也不需要关心是谁发送给它的。

在C#中使用Tag Message

在C#中模拟Erlang里的Tag Message很简单,其实就是把每条消息封装为Tag和参数列表的形式。同样的,我们使用的都是弱类型的数据——也就是object类型。如下:

public class Message
{
    public object Tag { get; private set; }

    public ReadOnlyCollection<object> Arguments { get; private set; }

    public Message(object tag, params object[] arguments)
    {
        this.Tag = tag;
        this.Arguments = new ReadOnlyCollection<object>(arguments);
    }
}

我们可以使用这种方式来实现一个乒乓测试。既然是Tag Message,那么定义一些Tag便是首要任务。Tag表示“做什么”,即消息的“功能”。在乒乓测试中,有两种消息,共三个“含义”。Erlang使用原子作为tag,在.NET中我们自然可以使用枚举:

public enum PingMsg
{ 
    Finished,
    Ping
}

public enum PongMsg
{ 
    Pong
}

在这里,我们使用简单的ActorLite进行演示(请参考ActorLite的使用方式)。因此,Ping和Pong均继承于Actor<Message>类,并实现其Receive方法。

对于Ping对象来说,它会维护一个计数器。每当收到PongMsg.Pong消息后,会将计数器减1。如果计数器为0,则回复一条PingMsg.Finished消息,否则就回复一个PingMsg.Ping:

public class Ping : Actor<Message>
{
    private int m_count;

    public Ping(int count)
    {
        this.m_count = count;
    }

    public void Start(Actor<Message> pong)
    {
        pong.Post(new Message(PingMsg.Ping, this));
    }

    protected override void Receive(Message message)
    {
        if (message.Tag.Equals(PongMsg.Pong))
        {
            Console.WriteLine("Ping received pong");

            var pong = message.Arguments[0] as Actor<Message>;
            if (--this.m_count > 0)
            {
                pong.Post(new Message(PingMsg.Ping, this));
            }
            else
            {
                pong.Post(new Message(PingMsg.Finished));
                this.Exit();
            }
        }
    }
}

对于Pong对象来说,如果接受到PingMsg.Ping消息,则回复一个PongMsg.Pong。如果接受的消息为PingMsg.Finished,便立即退出:

public class Pong : Actor<Message>
{
    protected override void Receive(Message message)
    {
        if (message.Tag.Equals(PingMsg.Ping))
        {
            Console.WriteLine("Pong received ping");

            var ping = message.Arguments[0] as Actor<Message>;
            ping.Post(new Message(PongMsg.Pong, this));
        }
        else if (message.Tag.Equals(PingMsg.Finished))
        {
            Console.WriteLine("Finished");
            this.Exit();
        }
    }
}

启动乒乓测试:

new Ping(5).Start(new Pong());

结果如下:

Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Finished

从上述代码中可以看出,由于没有Erlang的模式匹配,我们必须使用if…else…的方式来判断消息的Tag,接下来还必须使用麻烦而危险的cast操作来获取参数。更令人尴尬的是,与Erlang相比,在C#中使用Tag Message没有获得任何好处。同样是弱类型,同样得不到静态检查。那么好处在哪里?至少我的确看不出来。

有朋友可能会说,C#既然是一门强类型的语言,为什么要学Erlang的Tag Message?为什么不把Ping定义为Actor<PingMessage>,同时把Pong定义为Actor<PingMessage>呢?

呃……我承认,在这里使用Tag Message的确有种“画虎不成反类犬”的味道。不过,事情也不是您想象的那么简单。因为在实际情况中,一个Actor可能与各种外部服务打交道,它会接受到各式各样的消息。例如,它先向Service Locator发送一个请求,用于查询数据服务的位置,这样它会接受到一个ServiceLocatorResponse消息。然后,它会向数据服务发送一个请求,再接受到一个DataAccessResponse消息。也就是说,很可能我们必须把每个Actor都定义为Actor<object>,然后对消息进行类型判断,转换,再加以处理。

诚然,这种方法相对于Tag Message拥有了一定的强类型优势(如静态检查)。但是如果您选择这么做,就必须为各种消息定义不同的类型,在这方面会带来额外的开发成本。要知道,消息的数量并不等于Actor类型的数量,即使是如Ping这样简单的Actor,都会发送两种不同的消息(Ping和Finished),而且每种消息拥有各自的参数。一般来说,某个Actor会接受2-3种消息都是比较正常的状况。在面对消息类型的汪洋时,您可能就会怀念Tag Message这种做法了。到时候您可能就会发牢骚说:

“弱类型就弱类型吧,Erlang不也用的好好的么……”

F#中的模式匹配

提到模式匹配,熟悉F#的同学们可能会欢喜不已。模式匹配是F#中的重要特性,它将F#中静态类型系统的灵活性体现地淋漓尽致。而且——它还很能节省代码(这点在老赵以前的文章中也有所提及)。那么我们再来看一次F#在乒乓测试中的表现。

首先还是定义PingMsg和PongMsg:

type PingMsg = 
    | Ping of PongMsg Actor
    | Finished
and PongMsg = 
    | Pong of PingMsg Actor

这里体现了F#类型系统中的Discriminated Unions。简单地说,它的作用是把一种类型定义为多种表现形式,这个特性在Haskell等编程语言中非常常见。Discriminated Unions非常适合模式匹配,现在的ping对象和pong对象便可定义如下(在这里还是使用了ActorLite,而不是F#标准库中的MailboxProcessor来实现Actor模型):

let (<<) (a:_ Actor) msg = a.Post msg

let ping =
    let count = ref 5
    { new PongMsg Actor() with
        override self.Receive(message) =
            match message with
            | Pong(pong) ->
                printfn "Ping received pong"
                count := !count - 1
                if (!count > 0) then
                    pong << Ping(self)
                else
                    pong << Finished
                    self.Exit() }

let pong = 
    { new PingMsg Actor() with
        override self.Receive(message) =
            match message with
            | Ping(ping) ->
                printfn "Pong received ping"
                ping << Pong(self)
            | Finished ->
                printf "Fininshed"
                self.Exit() }

例如在pong对象的实现中,我们使用模式匹配,减少了不必要的类型转换和赋值,让代码变得简洁易读。还有一点值得顺带一提,我们在F#中可以灵活的定义一个操作符的作用,在这里我们便把“<<”定义为“发送”操作,避免Post方法的显式调用。这种做法往往可以简化代码,从语义上增强了代码的可读性。例如,我们可以这样启动乒乓测试:

ping << Pong(pong)

至于结果则与C#的例子一模一样,就不再重复了。

F#中的弱类型消息

可是,F#的世界就真的如此美好吗?试想,我们该如何实现一个需要接受多种不同消息的Actor对象呢?我们只能这样做:

let another = 
    { new obj Actor() with
        override self.Receive(message) =
            match message with
            
            | :? PingMsg as pingMsg ->
                // sub matching
                match pingMsg with
                | Ping(pong) -> null |> ignore
                | Finished -> null |> ignore
                
            | :? PongMsg as pongMsg ->
                // sub matching
                match pongMsg with
                | Pong(ping) -> null |> ignore
                
            | :? (string * int) as m ->
                // sub binding
                let (s, i) = m
                null |> ignore
                
            | _ -> failwith "Unrecognized message" }

由于我们必须使用object作为Actor接受到的消息类型,因此我们在对它作模式匹配时,只能进行参数判断。如果您要更进一步地“挖掘”其中的数据,则很可能需要进行再一次的模式匹配(如PingMsg或PongMsg)或赋值(如string * int元组)。一旦出现这种情况,在我看来也变得不是那么理想了,我们既没有节省代码,也没有让代码变得更为易读。与C#相比,唯一的优势可能就是F#中相对灵活的类型系统吧。

C#不好用,F#也不行……那么我们又该怎么办?

相关文章

Creative Commons License

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

Add your comment

33 条回复

  1. 老赵
    admin
    链接

    老赵 2009-07-13 09:12:00

    这……真的没有人想说什么吗?

  2. MoRota
    *.*.*.*
    链接

    MoRota 2009-07-13 09:29:00

    大概是不想装IE8吧。。我终于被你强迫的把IE6换成IE8了

  3. 老赵
    admin
    链接

    老赵 2009-07-13 09:41:00

    @MoRota
    恭喜您脱离了IE 6的魔爪。
    其实本来用IE 6只有20%略多一点,所以不太应该是封了IE 6的问题。

  4. FF的路过[未注册用户]
    *.*.*.*
    链接

    FF的路过[未注册用户] 2009-07-13 09:46:00

    胖子。。你真恶心哇。。。还好。。我安装了FF

    我也搞了一段时间的erlang。。。虽然语法古怪。。但是用着也不错。。。
    你现在是想用C#来实现Actor吗??
    何必呢。。。。每种语言都有自己擅长的方面。。。你又何必强求呢。。。

    也许我没懂你写这文章的目的。。。抱歉。。^_^

  5. guest[未注册用户]
    *.*.*.*
    链接

    guest[未注册用户] 2009-07-13 09:46:00

    楼主谈论的有点深奥...小辈们有点看不懂

  6. 老赵
    admin
    链接

    老赵 2009-07-13 09:55:00

    @FF的路过
    你没有理解错,我就是想在C#里实现Erlang style programming。
    不过这话别光对我说,也可以对那些实现Haskell,Python,Ruby,Scala Actor,并且用的如火如荼的人说嘛(Scala Actor相对最火了,Twitter都在用)。
    其实就是因为Erlang的某些缺点过于明显,所以要依靠一些高级语言来实现类似的功能,当然Erlang的优势也必须放弃一些。
    还有就是,Erlang的编程模型被证明为很适合某些情况,因此引进到其他高级语言/平台中,避免做任何事情都动用Erlang。
    现在Erlang周边讨论最热闹的东西之一,就是Erlang style programming啊。

  7. 别爱上哥,哥只是个传说!
    *.*.*.*
    链接

    别爱上哥,哥只是个传说! 2009-07-13 09:58:00

    个人认为,赵的那个姿势应该换个,进来老是觉得怪怪的,说不出的不舒服

  8. 老赵
    admin
    链接

    老赵 2009-07-13 09:59:00

    @别爱上哥,哥只是个传说!
    姿势?体位?你在说啥?

  9. CoolCode
    *.*.*.*
    链接

    CoolCode 2009-07-13 11:03:00

    对Erlang不懂啊,只能飘过

  10. 孟兆斌
    *.*.*.*
    链接

    孟兆斌 2009-07-13 11:04:00

    呵,我路过心里有一疑问,最近在做下总结项目经验写了大半宿都没个样子出来,借老赵的宝地来提下这个尴尬的问题,项目中感觉技术含量很深的东西没有但是真的很有特色,比如最近做的一个东西用Stocket和delphi写的中间层进行数据传输,数据量太大就没用C#来写数据处理的算法,这里举个例子,很是郁闷这样的项目如何去介绍,请类似如BOSS ZHAO解答下哈

  11. 别爱上哥,哥只是个传说!
    *.*.*.*
    链接

    别爱上哥,哥只是个传说! 2009-07-13 11:07:00

    没.

  12. 汝熹
    *.*.*.*
    链接

    汝熹 2009-07-13 11:08:00

    为什么没有收藏的连接了?

  13. guest[未注册用户]
    *.*.*.*
    链接

    guest[未注册用户] 2009-07-13 11:24:00

    @Jeffrey Zhao

    别爱上哥,哥只是个传说!:个人认为,赵的那个姿势应该换个,进来老是觉得怪怪的,说不出的不舒服


    =======================应该是人家觉得你老举着个手,觉这么久了,也应该累了 哈哈哈

  14. 老赵
    admin
    链接

    老赵 2009-07-13 11:32:00

    CoolCode:对Erlang不懂啊,只能飘过


    其实这些东西只提到了一点Erlang,大部分内容还是C#,讨论的东西也是很容易的,不是吗?

  15. 老赵
    admin
    链接

    老赵 2009-07-13 11:36:00

    @孟兆斌
    介绍我不知道咋写。但是项目总结可以写的东西有很多啊,比如:

    为什么用Delphi写?优势在哪里?
    C#就完全处于劣势吗?有没有某种情况下完成类似工作适合使用C#?
    看上去是因为性能问题使用Delphi,那么有没有具体评测?(不同数据量下,不同连接数,CPU耗费,内存耗费……)
    项目的优点和缺点?未来的改进?有没有弯路?
    有没有弯路?如果让你从头开始写的话,你会怎么做?
    会不会尝试某些开源项目?会不会尝试把你的项目总结成开源?

  16. 老赵
    admin
    链接

    老赵 2009-07-13 11:39:00

    guest:应该是人家觉得你老举着个手,觉这么久了,也应该累了 哈哈哈


    兄弟我不够帅,拍不出好照片啊。

  17. 孟兆斌
    *.*.*.*
    链接

    孟兆斌 2009-07-13 11:42:00

    @Jeffrey Zhao
    thk,还有一点,怕写太细节呢人烦,无从下笔的感觉,以前招人的时候看着太长的案例介绍就发晕直接找英文单词的技术点.

  18. 老赵
    admin
    链接

    老赵 2009-07-13 11:54:00

    @孟兆斌
    这要看你写作对象是谁了,简历当然不应该太复杂,不是说,甚至应该在一页以内吗?

  19. 代震军
    *.*.*.*
    链接

    代震军 2009-07-13 14:20:00

    老赵关注的越来越广了,我都跟不上了,呵呵。

  20. 别爱上哥,哥只是个传说!
    *.*.*.*
    链接

    别爱上哥,哥只是个传说! 2009-07-13 14:54:00

    坐正了,拍个照,比这个好,为啥老用个Y手势呢?你说呢,赵同志?

  21. 老赵
    admin
    链接

    老赵 2009-07-13 15:24:00

    @别爱上哥,哥只是个传说!
    不是“老用”,而是只有这张,咱总是用最新的照片,呵呵。
    如果您不习惯的话暂时还是忍忍吧,啥时我再拍张照片,一定换上!

  22. 老赵
    admin
    链接

    老赵 2009-07-13 15:53:00

    今天这篇文章的阅读量怎么那么多?其中只有20%是RSS订阅。

  23. 勇赴
    *.*.*.*
    链接

    勇赴 2009-07-13 16:46:00

    "下"怎么点不进去?

  24. 勇赴
    *.*.*.*
    链接

    勇赴 2009-07-13 16:51:00

    F#和C#有什么区别呢?第一次见F#

  25. 野男人
    *.*.*.*
    链接

    野男人 2009-07-13 19:46:00

    左边的模板太大啦。。。

  26. 老赵
    admin
    链接

    老赵 2009-07-13 20:35:00

    @野男人
    好像一般人总是抱怨右边大了……

  27. trust[未注册用户]
    *.*.*.*
    链接

    trust[未注册用户] 2009-07-14 17:40:00

    老赵大哥,碰到个二级域名的问题,能不能帮我诊断下,痛苦死了。非常感谢 qq103763768.

  28. 老赵
    admin
    链接

    老赵 2009-07-14 17:50:00

    @trust
    没有qq,可以使用右下角的web messenger,最好是email。

  29. iceboundrock
    *.*.*.*
    链接

    iceboundrock 2009-07-16 23:51:00

    我假想一下,如果有一个hashtable,里面放了所有的tag和对应的消息处理delegate,也许会让代码看起来清爽点。注册delegate的事情可以在启动的时候完成,完了之后这个数据结构就成为readonly的了,也没有多线程的问题。

  30. 老赵
    admin
    链接

    老赵 2009-07-17 00:01:00

    @iceboundrock
    并不是那么简单的。
    假设一个场景,A,B,C三种Actor,每种均有多个实例。
    A向B发送m1,m2两种消息。
    B -> C:m2,m3
    C -> A:m3,m1
    也就是说,无论是发送消息还是接受消息,都是完全和Actor种类没有对应关系的。
    而且还要考虑几点:易于编程,易于维护,容易单元测试,容易重构,编译期校验……

    Tag Message是Erlang的方式,这里只是模拟一下.而且Erlang是动态类型语言,很难静态检查,所以并不是要从这个方向入手,呵呵。

  31. Laser.NET
    *.*.*.*
    链接

    Laser.NET 2009-09-20 12:00:00

    有一点不太理解,楼主为什么一定要用泛型,其实那样反而觉得怪怪的。

    其实真实应用中,你找不到哪种情况下所有的消息都是同一个类型的,也不可能做到所有的消息类型都在程序接口中固化下来。所以类型的强制转化是在所难免的。

    另外在分布式应用中,更大的性能消耗是在消息数据的序列化和反序列化上,那个上面消耗的时间是类型转换和检查的几万倍甚至更多。

    所以我觉得过分的强调在接口中就能够完美地固化所有消息类型是不现实的,反而会产生累赘感。

    Actor模型是一种避免数据共享从而有效避免死锁的一种方式。如果真的要全面模拟erlang,更多的还要考虑消息的传递效率,线程的执行效率,还有如何做到底部透明并且灵活的分布式部署。

    我最近也在用C#模拟实现erlang style programming,希望能够和楼主多讨论,多学习!:)

  32. Laser.NET
    *.*.*.*
    链接

    Laser.NET 2009-09-20 12:16:00

    而且真实应用中,绝大部分消息都是xml或json格式的复杂消息数据。这些是不可能做到静态类型检查的。其实动态数据格式也有它最大的优点,更加灵活并且易于扩展,这个也是很重要的。静态类型固化和检查所带来的那么一点执行效率的提升在这些分布式应用场景中是非常的微乎其微的。

    erlang那种tuple(或者是衍生出的record数据)很容易转换成json格式,并且能够很灵活的增删数据字段。在网络传输的时候应该尽可能的精简消息数据,用到哪些字段就动态拼接出那些字段,冗余的字段就可以忽略。这个也是erlang可以比较容易的和javascript整合并且在互联网上开始流行的一个重要原因。

  33. Laser.NET
    *.*.*.*
    链接

    Laser.NET 2009-09-20 12:46:00

    至于说静态类型的固化带来的开发效率的提高,比如可以利用VS的智能感知(Intellisense)和代码重构(Refactoring)等好处确实是有的。但是要做到这个是需要付出代价的。

    首先你需要架构和设计师一开始就固化出所有可能用到的消息数据的静态类型。这个几乎在真实的应用中是很难做到的。(举个例子,LinQ中引入动态匿名类型就是因为不可能将查询中所需要用到的所有可能数据类型都固化。javascript的动态特性更加适合浏览器上的开发,如果将DOM结构和CSS那些复杂的数据结构和字段都固化清楚,那将需要掌握几百几千个类型,但是对于用js的开发人员只需要记住比较通用的几十个属性和方法,而不需要管具体是什么静态类型)

    其次,很多真实应用场景中用到哪些数据字段甚至是要等到运行时才可以确定的。尤其在互联网或分布式应用中,大量的通信数据本身都是动态的,为了提高网络通信的效率,更需要能够动态的拼接和删减数据字段。当需要扩展的时候也能够比较容易的增加一些字段(比如XMPP之所以号称extensible,就是因为它的数据格式是xml,并且用户可以在xml里面自己添加自定义的任何节点。并且只传输用到的xml节点和属性,用不到的忽略。并且对外的接口永远就是那么三四个主要的xml格式presence/message/iq,扩展和维护只需要改程序的内部实现,不需要改动接口)。

    如果过分的强调在前期接口设计中就固化或者静态化所有数据类型,短期内似乎是可以通过VS等专用工具提高开发和维护效率。但是真正应用后你会发现很多时间都花在静态类型的设计和重构上了(不见得方便多少,甚至更加麻烦),并且失去了系统扩展的灵活性。而且还会过于依赖VS这种专有自动化工具。

    所以我个人觉得:简单的小型应用,强调应用类型检查是很有好处的。但是对于大型的复杂的分布式应用,有的时候动态数据类型或许有更多的好处,尤其是灵活性和可扩展性。

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我