Hello World
Spiga

适合C# Actor的消息执行方式(4):阶段性总结

2009-07-20 09:19 by 老赵, 6733 visits

这个系列原本打算写3篇,也就是说在上一篇文章中已经把老赵认为较合适的方法展现出来了,但事实上这个系列的计划已经扩展为8篇了——没错,翻了一倍不止,而最终是几篇现在我也无法断言。其实这也是我写不了书的原因之一。虽说唯一不变的就是变化,但是我变的太离谱了。不断写,不断出现新想法,不断变化。作为这个系列的第4篇,我们现在来对之前3篇文章进行一番阶段性总结。

阶段性总结本不在计划之内,不过似乎Actor模型这方面内容还不太受人关注,因此有的朋友也误解这系列文章想要解决的问题是什么。除了这方面的解释之外,我还会对之前提出的几种做法进行综合的对比,可以进一步了解整个演变过程的思路,为接下去的改变做铺垫——因为下次改变就涉及到多个方向,每个方向都是在一定程度上真正可用的方式。

我们究竟是要解决什么问题

Actor模型的本质已经被强调了无数遍:万物皆Actor。Actor之间只有发送消息这一种通信方式,例如,无论是管理员让工作者干活,还是工作者把成果交还给管理员,它们之间也要通过发送消息的方式来传递信息。这么做看似不如直接方法调用来的直接,但是由于大量的消息可以同时执行。同样,消息让Actor之间解耦,消息发出之后执行成功还是失败,需要耗费多少时间,只要没有消息传递回来,这一切都和发送方无关。Actor模型的消息传递形式简化了并行程序的开发,使开发人员无需在共享内存(确切地说,其实是共享“写”)环境中与“锁”、“互斥体”等常用基础元素打交道。不过,使用Actor模型编写应用程序,需要开发人员使用一种与以往不同的设计思路,这样的思路说难倒不难,说简单也不简单。等我们有了成熟、稳固的Actor模型之后(例如高效的调度,合适的容错机制,老赵正在为此努力),再回头来探究这种特殊的架构方式。

由于Actor执行的唯一“事件”便是接受到了一个消息,而一个Actor很可能会做多件事情,因此我们一定需要一种机制,可以把消息“分派”到不同的“逻辑段”中去,并为不同的逻辑指定各自所需要的参数。例如,Person是一个Actor类型,它有三种任务,不同的任务会带有不同参数:

  1. 聊天(Chat):指定另一个Person对象(聊天的另一方),以及一个Topic对象(聊天的话题)。
  2. 吃饭(Eat):指定一个Restaurant对象(餐馆)。
  3. 干活(Work):指定一个Person对象(工作完成后的汇报人),以及一个Job对象(任务)。

当Person对象获得一条消息时,它需要将其识别为聊天、吃饭或干活中的一种,再从中获取到这个行动所需要的数据。如果用一幅示意图来表示,它可能是这样的:

message dispatch.png

如何在C#中把一条消息转化为一段逻辑的执行,并且尽可能确保一些优势(如易于编写,静态检查,代码提示,重构,单元测试……),这便是这系列文章唯一的目的。正如文章的标题,我们关注的是“消息执行方式”,而不是:

  • “消息传递”与“共享内存”两种并行方式的比较
  • 讲述Actor模型的应用程序设计方式。
  • 提出消息传递时的解耦方式。
  • ……

文章使用Actor模型作为示例,是因为我编写的ActorLite组件易于说明问题,并且是典型的“消息传递”场景。事实上,文章所表达的内容,适合任何基于消息传递的C#场景,例如内存中的消息队列、生产者/消费者模式、消息总线……它并没有限制Actor模型这一种架构方式。

Erlang的模式匹配

首先,我们观察了Erlang中的模式匹配。在Erlang中,一个消息往往为一个元组,而一个Actor便会根据这个消息的模式,或者用更通俗的方式来讲,“结构”,来选择处理消息的逻辑分支。例如对于上面举出的例子,它的模式匹配代码便可能是:

receive
    {chat, Person, Topic} ->
        ... % “聊天”逻辑
    {eat, Restaurant} ->
        ... % “吃饭”逻辑
    {work, Person, Job} ->
        ... % “干活”逻辑
end

小写字母开头的标识符为“原子”,可以认为是一个常量,用于标识这个消息用来“干什么”。大写开头的为“绑定”,可以认为是一个变量(虽然不可变),用于标识这个消息“所使用的数据”。如果使用示意图来表示这个消息执行方式,则类似于:

erlang pattern matching

如果收到的消息是{eat, {mcdonalds, 2}},则会执行“吃饭”逻辑,而执行时Restaurant的值将自动绑定为元组{mcdonalds, 2},而不需要任何转化或赋值操作。Erlang便是这样将一个消息转化为一段逻辑执行的。

C#的Tag Message

一般来说,Erlang的消息是一个元组,而元组的第一个元素为原子,用来标识“做什么”。这个原子被称为是这个消息tag,这种用法被叫做Tag Message,它是“Erlang编程规范”中的推荐用法。在C#中,我们当然也可以这么做

class Person : Actor<Message>
{
    protected override void Receive(Message message)
    {
        if (message.Tag == "Chat")
        {
            Person another = (Person)message.Arguments[0];
            Topic topic = (Topic)message.Arguments[1];
            // ...
        }
        else if (message.Tag == "Eat")
        {
            Restaurant restaurant = (Restaurant)message.Arguments[0];
            // ...
        }
        else if (message.Tag == "Work")
        {
            Person reportTo = (Person)message.Arguments[0];
            Job job = (Job)message.Arguments[1];
            // ...
        }
    }
}

图示如下:

message dispatch.png

这个方式和Erlang可谓如出一辙,但是由于缺少了Erlang的模式匹配和自动绑定,于是C#代码需要大量的if…else判断,以及繁琐而危险的转型操作。此外,和Erlang中动态类型的缺点完全相同,无论是消息的发送还是接受完全不是静态类型的,因此无论是静态检查,编辑还是重构都比较困难。试想,如果一个公用的服务所接受的消息结构改变了,那么所有用到它的地方都必须修改正确——如果缺少静态检查,错误都只能在运行时才能发现。Erlang有着强大的动态升级能力,尚可接受不断地在线更新。而在.NET平台中,如果使用这种Tag Message的方式,待到运行时发现错误,要修改起来就比较麻烦了。

强类型消息

为了避免繁琐的转型,为了获得类型安全的各种优势,我们也可以选择为每种不同的消息创建独立的类型。不过由于一个Actor往往会应对各种消息,因此在.NET环境中,往往我们需要把消息类型定义为object。如果使用ActorLite来演示的话,代码可能是这样的:

class Person : Actor<object>
{
    protected override void Receive(object message)
    {
        if (message is ChatMessage)
        {
            ChatMessage chatMsg = (ChatMessage)message;
            Person another = chatMsg.Another;
            Topic topic = chatMsg.Topic;
            // ...
        }
        else if (message is EatMessage)
        {
            EatMessage eatMsg = (EatMessage)message;
            Restaurant restaurant = eatMsg.Restaurant;
            // ...
        }
        else if (message is WorkMessage)
        {
            WorkMessage workMsg = (WorkMessage)message;
            Person reportTo = workMsg.ReportTo;
            Job job = workMsg.Job;
            // ...
        }
    }
}

图示如下:

string typing message

使用if…else来进行逻辑分支判断还是必要的,不过我们这里使用了静态类型代替了Magic String(当然在使用Tag Message时也可以使用常量)的判断,同时危险而麻烦的类型转换操作也减少的。与Tag Message相比,这种做法获得了一定的类型安全优势,可以得到编译器的静态检查,做起重构来也有了依据。不过他也有比较明显的缺陷,那就是需要构建大量的消息类型。要知道消息类型的数量很可能是Actor类型数量的几倍,每种消息类型还包含着多个属性,构造函数接受参数,然后在构造函数里设置属性……这种做法对复杂性的提升还是较为可观的,有时候会感觉还不如使用简单的Tag Message。

接口、协议及消息

消息,其实是两个Actor之间定下的协议,一个Actor可以实现多种协议。这样的对应关系使人联想到.NET中的接口。因此我们可以使Actor实现某个接口,一条消息其实就是使用“委托”来告诉Actor该做什么事情。一个“委托”对象也可以自然地携带执行时所用的数据。这似乎满足我们的要求。使用这种方式来实现的消息执行大概是这样的

interface IPersonMessageHandler
{
    void Chat(Person another, Topic topic);
    void Eat(Restaurant restaurant);
    void Work(Person reportTo, Job job);
}

class Person : Actor<Action<IPersonMessageHandler>>, IPersonMessageHandler
{
    protected override void Receive(Action<IPersonMessageHandler> message)
    {
        message(this);
    }

    #region IPersonMessageHandler Members

    void IPersonMessageHandler.Chat(Person another, Topic topic) { ... }

    void IPersonMessageHandler.Eat(Restaurant restaurant) { ... }

    void IPersonMessageHandler.Work(Person reportTo, Job job) { ... }

    #endregion
}

图示如下:

interface contract

使用这种方式似乎带来的许多好处,例如我们使用接口这个非常轻量级的特性实现了消息,无须编写额外的代码将消息转化为逻辑。此外,接口是强类型的,适合编译期检查,易于重构和单元测试,还为我们带来“消息组”这样一种简单的消息管理方式——似乎就是我们理想的消息执行方式啊。是啊,这是很美好的消息执行方式,但是……为什么说它“中看不中用”?

这里带来的最大问题在于耦合地过于强烈。例如Chat消息的第一个参数是Person,表示聊天的对象。但是很可能在同一个系统中,可以聊天的对象不一定仅限于是人(Person),还可能是一个机器人(Robot);同理Work的汇报者也可能是一个记录系统。事实上,我们可能只要求Chat的目标是一个可以处理IChater消息的Actor即可,这意味着Chat方法的第一个参数的类型需要是Actor<Action<IChater>>。但是,这个Actor还需要接受其他类型的消息,如IWorkReportHandler,这又意味着它的类型需要是Actor<Action<IWorkResultHandler>>。一个Actor又如何成为两种类型?

在实际运用中,这点无法回避,因此我们必须得变。

使用“消息总线”来解耦?

风云兄提出,既然问题的关键在于强耦合,为什么不使用消息总线来解耦呢?其实这问题很容易回答。

首先,“强耦合”并不是我们想要解决的问题。我们想要解决的是“消息执行”(见文章标题)而不是“消息传递”,“强耦合”只是我们得到满意的解决方案之前所遇到的困难而已。“消息总线”是消息传递时,系统(大粒度)组件之间的解耦方式。而现在我们要解开的,是Actor这种小粒度对象之间消息传递造成的耦合。在.NET消息传递过程中,消息是一个对象,一般在框架中使用TMessage表示。例如风云兄给出的代码,TMessage即为字符串。我们的目标是如何从一个TMessage类型的对象(如字符串)分配至不同的逻辑片断——还要携带参数过去。风云兄的例子回避了这一点。

事实上,正如文章一开始所说的那样,我们文章得出的解决方案并不仅限于Actor模型,它适合各种消息传递场景——这些场景里自然包括“消息总线”的使用。关于文章的目的,“亚历山大同志”同学称之为“要在C#里面实现优雅的模式匹配的问题”。从一定角度上来说,老赵认为这个描述非常妥当,因为Erlang中模式匹配的目的便是消息执行。

其实这可能也是基于Actor模型的程序架构方式还不为人熟悉的缘故。其实“消息总线”和“Actor模型”间的关系……不大,其相似性大概也只有“消息传递”这个特性而已。但是,在Actor模型中,消息是Actor对象间通信的唯一方式。Actor模型在使用时,内存中可能产生成千上万个Actor对象,它们互相发送消息产生交互。同时,Actor可以随时被创建出来,用完后又可以随时丢弃,因此Actor之间的通信无法“预先指定”。而“消息总线”需要在运行之前进行“注册”,然后它可以控制一条消息的Subject(即目标)。如果使用消息总线来实现Actor模型的话,则必须在Actor对象创建出来以后“注册”到消息总线,然后在Actor销毁之后“解开”。这又要求消息总线拥有高效的“注册”和“解开”操作,还必须完全是线程安全的。

正是这个原因,Actor模型在使用时一般都是得到对方的引用并“直接”发送。而且,会把自己作为消息的一部分传递过去,这是为了让对方可以“直接”回复,这带来了程序设计过程中相当的灵活性。当然,这条消息可能会暂时放在Actor的队列中,等Actor空闲时再执行。这又是Actor模型的又一个特性:对于单个Actor来说,消息的执行完全是线程安全的。这大大简化了并行程序设计的难度,也是它与“共享内存”这种并行程序设计方式的区别。如果使用消息总线来实现Actor模型,它可以保证向一个Subject快速发送两条消息后,它们被依次执行吗?

因此,如乒乓测试这种简单的消息传递示例,可以使用消息总线来实现,而复杂的场景就不合适了。在下一篇文章中,老赵会使用Actor模型来实现一个相对复杂的示例:网页小爬虫。在添加功能的过程中,您一定可以更好的了解Actor模型的使用方式,以及它在并行程序设计时的优势。

相关文章

Creative Commons License

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

Add your comment

45 条回复

  1. CoolCode
    *.*.*.*
    链接

    CoolCode 2009-07-20 09:25:00

    支持了再看!好文!

  2. 老赵
    admin
    链接

    老赵 2009-07-20 09:29:00

    @CoolCode
    谢谢

  3. wuliangbo
    *.*.*.*
    链接

    wuliangbo 2009-07-20 10:02:00

    嗯 不错 一直在关注这个系列

  4. 亚历山大同志
    *.*.*.*
    链接

    亚历山大同志 2009-07-20 10:41:00

    突然想到一个问题,看回头有空发文探讨一下

    貌似在C#中函数不是Top Level的,所以感觉在用的时候怪怪的。
    另Erlang中的匹配不是一次就完事的,很有可能会递归进行,这个不知道该如何处理的

  5. 老赵
    admin
    链接

    老赵 2009-07-20 10:46:00

    @亚历山大同志
    其实我觉得,用C#等语言来实现Erlang Style Programming主要学的是它的Concurrency Oreiented Programming的方式,倒不是为了模拟Erlang的语法。
    Erlang的关键还是在于消息传递,并发,容错,热升级等特点上,倒不是它的语法,我认为。
    所以我对于消息执行方式的目标是,如何找出一种合适的方法来执行消息,可以方便开发,静态检查,重构等等。
    这方面讨论的比较缺,因为大家主要把目光放在message passing方面,但我觉得消息执行也是工程上不可回避的东西。

  6. 老赵
    admin
    链接

    老赵 2009-07-20 10:50:00

    @亚历山大同志
    对了,你说“递归进行”,我理解你的意思是这两点:
    1、尾递归,就是接受并执行完一个消息后,去执行下一个。
    2、selective receive。
    其实这个就要看这个Actor框架的API设计了。
    比如ActorLite,它对于1是靠框架不断调用Receive(msg)方法来实现的,使用者很难控制,而对于2,ActorLite不支持……
    我现在也在想另一套更好的Actor API,可以支持Selective Receive,因为它真的很重要。
    在C#等语言中,要实现selective receive真不容易啊,比如Scala就不支持。F#支持,因为使用了特别的语言特性(workflow,类似于monad)。

  7. Bhunter
    *.*.*.*
    链接

    Bhunter 2009-07-20 10:55:00

    那究竟哪种合适? 看完了还是不太明白

  8. 老赵
    admin
    链接

    老赵 2009-07-20 11:05:00

    @Bhunter
    都合适,也都不合适。
    事实上每种做法都是可行的,文章里给出的就是各种做法的优点和缺点啊,很多时候可以自行选择的。
    就好比我自己,随意写的小程序会用的是“接口、协议及消息”,某些情况下会直接使用“强类型消息”;复杂的工程会用……还没有公布的另外两种;Tag Message很少用。

  9. Galactica
    *.*.*.*
    链接

    Galactica 2009-07-20 11:21:00

    感觉就像进程内的WCF。

  10. Bhunter
    *.*.*.*
    链接

    Bhunter 2009-07-20 11:27:00

    @Galactica
    不限于进程内吧?

  11. 老赵
    admin
    链接

    老赵 2009-07-20 11:27:00

    @Galactica
    哪方面像?

  12. 风云
    *.*.*.*
    链接

    风云 2009-07-20 16:25:00

    @Jeffrey Zhao
    很高兴你对消息总线提出的理解和建议,我将对以下问题逐一回答

    1. 我们的目标是如何从一个TMessage类型的对象(如字符串)分配至不同的逻辑片断——还要携带参数过去。风云兄的例子回避了这一点。

    回答:消息总线,接收的消息是可以携带参数的,而且可以是任何类型的,可以是弱类型也可以是强类型的,可能我没有把详细代码发布出来,所以老赵兄产生误会了,呵呵

    2. 我们文章得出的解决方案并不仅限于Actor模型,它适合各种消息传递场景——这些场景里自然包括“消息总线”的使用。关于文章的目的,“亚历山大同志”同学称之为“要在C#里面实现优雅的模式匹配的问题”

    回答:消息总线在之前的版本对模式匹配是不支持的,现在吸收了微软企业库CAB中消息总线的标签模式的模式匹配思想,进行了改造,现在是可以支持模式匹配的

    3. 其实“消息总线”和“Actor模型”间的关系……不大,其相似性大概也只有“消息传递”这个特性而已。

    回答: Actor 模型中的Actor 和消息总线间的关系确实不大,但是和消息总线中的Subject是非常相似的,都有发送消息和接受消息,显著不同的是Actor的接收是一个抽象函数,就是说你能在派生类中处理接收,它的接收者只是它自身,是一种单播行为,Subject的接收是一个多播的委托函数,单播是多播的一个特例,所以说Subject是可以模拟出ActorLite中的Actor的。

    4. 但是,在Actor模型中,消息是Actor对象间通信的唯一方式。

    回答:这句话在消息总线中也是完全正确的

    5. Actor模型在使用时,内存中可能产生成千上万个Actor对象,它们互相发送消息产生交互。同时,Actor可以随时被创建出来,用完后又可以随时丢弃,因此Actor之间的通信无法“预先指定”。

    回答: 消息总线模型在使用时,内存中也可能产生成千上万个Subject对象,它们互相发送消息产生交互。同时,Subject可以随时被创建出来,用完后必须通过路由表来进行卸载,因此Subject之间的通信必须通过注册来预先指定接收者。
    Actor之间的通信无法“预先指定”,我认为是需要的,Actor之间必须要预先知道对方,因为各方都实现了Receive 接收函数的。

    6. 如果使用消息总线来实现Actor模型的话,则必须在Actor对象创建出来以后“注册”到消息总线,然后在Actor销毁之后“解开”。这又要求消息总线拥有高效的“注册”和“解开”操作,还必须完全是线程安全的。

    回答:分析的很到位,如果在单线程环境中,是不需要线程安全的,消息总线是完全可以定制的,可以定制线程安全的或者是不安全的。


    6. 如果使用消息总线来实现Actor模型,它可以保证向一个Subject快速发送两条消息后,它们被依次执行吗?

    回答:我上次所画的消息总线结构图,是定义了消息总线的API接口,很多API接口(存储转发所用的队列)我是都隐藏了,仅仅显示了一些关键的API接口, 消息总线从设计之初已经考虑了队列化消息了,所以你的问题是可以解决的。

    7. 如乒乓测试这种简单的消息传递示例,可以使用消息总线来实现,而复杂的场景就不合适了。

    回答:复杂的场景用消息总线是有点麻烦,该消息总线我已经在项目中使用3年多了,因为消息总线是多播的,Actor是直接通讯的(类似单播的),所以要实现直接通讯并且是单播行为的,必须借助于消息总线的钩子来过滤观察者来模拟单播行为,在我们的项目中这种情况也是很多的。

    8. selective receive

    回答: 消息总线通过很多中途径来实现selective receive,有前置过滤器,过滤器,以及定义消息钩子来实现

  13. 风云
    *.*.*.*
    链接

    风云 2009-07-20 16:29:00


    先附上本文例子代码以及运行结果,希望多多交流心得,现在项目稍微有点闲,我现在正在重构消息总线的代码,不久我会发布出来,希望多多指教:


    static IMessageRouter Actor;
    
            [SetUp]
            public void Setup()
            {
                Actor = new MessageRouter();
                Actor.HookManager.RegisterHook(new ChatHook(), new EatHook(), new WorkHook());
            }
    
            private class ChatHook : HookAdapter
            {
                public ChatHook()
                {
                    HookType = HookType.MessageReceiving;
                }
    
                public override void OnMessageReceiving(object sender, MessagePacket e)
                {
                    Topic<ChatMessage> msg =  e.Message as Topic<ChatMessage>;
                    if (msg == null)
                        return;
                    if (object.ReferenceEquals(sender, msg.Message.Another))
                    {
                        e.Canceled = true;
                        return;
                    }
                    if (!object.ReferenceEquals(msg.Message.Another, e.Handler.Target))
                    {
                        e.Canceled = true;
                    }
                }
            }
    
            class Person:IPersonMessageHandler
            {
                public string Name { get; set; }
                public Person()
                {
                    Actor.Subscribe<ChatMessage, EatMessage, WorkMessage>(
                        Topics.Chat, (s, e) =>
                        {
                            var sender = s as Person;
                            Console.WriteLine(string.Format("[{0}] 正在和[{1}] 谈论[{2}].", sender.Name, e.Message.Another.Name, e.Message.Topic));
    
                            const string exitContent = "烦不烦,天天讨论天气,拜拜!!!!";
                            if (e.Message.Topic.Content == exitContent)
                                Actor.RouterTable.Remove<ChatMessage>(Topics.Chat);
                            else
                                Chat(sender, new Topic { Name = e.Message.Topic.Name, Content = exitContent });
                        },
                        Topics.Eat, (s, e) =>
                        {
                            Console.WriteLine(string.Format("[{0}] 正在[{1}] 的 酒店就餐.", Name, e.Message.Restaurant.Address));
                        },
                        Topics.Work, (s, e) =>
                        {
                            Console.WriteLine(string.Format("[{0}] 正在为他的上级[{1}] 做[{2}].", Name, e.Message.ReportTo.Name,e.Message.Job.Name));
                        }
                        );
                }
    
                public void Chat(Person another, Topic topic)
                {
                    Actor.Publish<ChatMessage>(Topics.Chat, this, new ChatMessage { Topic = topic, Another = another });
                }
    
                public void Eat(Restaurant restaurant)
                {
                    Actor.Publish<EatMessage>(Topics.Eat, this, new EatMessage { Restaurant = restaurant });
                }
    
                public void Work(Person reportTo, Job job)
                {
                    Actor.Publish<WorkMessage>(Topics.Work, this, new WorkMessage { Job = job, ReportTo = reportTo });
                }
    
                public override string ToString()
                {
                    return Name;
                }
            }
    
           
            [Test]
            public void Test()
            {
                var zhangSan = new Person { Name = "张三" };
                var liSi = new Person { Name = "李四" };
                var wangWu = new Person { Name = "王五" };
    
                zhangSan.Chat(liSi, new Topic { Name = "天气", Content = "今天天气也太热了,真受不了。。。。" });
                wangWu.Eat(new Restaurant { Address = "王府井大街" });
    
                liSi.Work(wangWu, new Job { Name = "客服" });
            }
    
    
    


    下面是输出结果:
    [张三] 正在和[李四] 谈论[天气 Content:今天天气也太热了,真受不了。。。。].
    [李四] 正在和[张三] 谈论[天气 Content:烦不烦,天天讨论天气,拜拜!!!!].
    [王五] 正在[王府井大街] 的 酒店就餐.
    [李四] 正在为他的上级[王五] 做[客服].

  14. 老赵
    admin
    链接

    老赵 2009-07-20 16:34:00

    @风云
    以后我会慢慢给出更复杂的示例,到时候再看看吧。:)
    对了,不知道你有没有了解过Erlang,其实我的目标便是实现Erlang方式的面向并发编程,因此需要很大的灵活性和易用性。
    包括你说的selective receive,不知道是否是Erlang形式的呢?

  15. 风云
    *.*.*.*
    链接

    风云 2009-07-20 16:50:00

    Erlang很早看过,我的消息总线和Retlang/Jetlang是相似的,不过它的目标也是面向并发编程,我的消息总线对并发是可以选择的,

    selective receive:
    receive
    {chat, Person, Topic} ->
    ... % “聊天”逻辑
    {eat, Restaurant} ->
    ... % “吃饭”逻辑
    {work, Person, Job} ->
    ... % “干活”逻辑
    end

    这种模式匹配,我给你的例子已经演示了,是支持的,这种支持我是今天研究了一上午才支持的,之前支持类似这种行为
    receive
    {chat, Person, Topic} ->
    ... % “聊天”逻辑
    end

    receive
    {eat, Restaurant} ->
    ... % “吃饭”逻辑
    end

    receive
    {work, Person, Job} ->
    ... % “干活”逻辑
    end

    然后把这种行为进行一些简单的组合就形成了类似Erlang的那种写法了,呵呵!

  16. 老赵
    admin
    链接

    老赵 2009-07-20 16:56:00

    @风云
    你定义了各Message类(如ChatMessage),这就是因为TMessage只能携带一个对象的缘故阿。我的方法就是通过委托轻松地携带了多个参数,其实你也可以用这种方法。就像我说的,我的文章内容适合很多消息传递的场景。
    我现在对你的消息总线实现蛮感兴趣的,希望可以详细分析一下。不过你给的这段代码显得有些复杂,又要定义Hook……所以我还是认为难以满足Actor模型中面向并发编程的需求,这方面还是使用Actor框架好,用消息总线模拟会很累。
    不是一种场景,就算可以使用,也不合适啊。例如,消息总线定位成千上万的的Subject需要额外开销。不断地注册和注销Subject为了保证线程安全往往需要加锁,这降低伸缩性。
    用消息总线实现Actor的感觉,就有点像用火箭筒打巷战。我们要的是使用灵活轻型的小手*枪,不是威力强大的重型武器,呵呵。

    这是我的代码:http://gist.github.com/150216
    // 你也贴到github上去吧,博客园对于太长代码不太友好,而且有bug……

  17. 老赵
    admin
    链接

    老赵 2009-07-20 17:06:00

    @风云
    selective receive不是那么简单的啊,你给出的erlang代码还是普通的pattern matching而已。
    selective receive是会改变消息执行顺序的,就是有选择的略过一些,放到以后再执行,你给的例子,是并行的match。如这个:

    我们设想有这样一个服务器集群,其中有一台Master,若干台Worker,若干台Client。
    Client 向 Workder 要求他做 sth. 的时候,Worker 要求 Master 返回一些 info,然后Worker 继续完成 sth.

    看下Worker这个服务器:

    loop() ->
     receive
      {Client, DoSth} ->
       master ! {self(), need_info},
       receive
        {master, Info}
       end,
       continue_to_do,
       loop();
     end.

    这里有两个receive,其中第一个receive响应client的请求,第2个receive取得master回复的Info。在receive master的回复之前,很可能有其他Client请求过来,但是由于第2个receive并不处理这些请求,所以这些请求消息被放到save queue中,直到master回复后,这些请求被放回mailbox。

  18. 风云
    *.*.*.*
    链接

    风云 2009-07-20 17:36:00

    Jeffrey Zhao:
    @风云
    你定义了各Message类(如ChatMessage),这就是因为TMessage只能携带一个对象的缘故阿。我的方法就是通过委托轻松地携带了多个参数,其实你也可以用这种方法。就像我说的,我的文章内容适合很多消息传递的场景。
    我现在对你的消息总线实现蛮感兴趣的,希望可以详细分析一下。不过你给的这段代码显得有些复杂,又要定义Hook……所以我还是认为难以满足Actor模型中面向并发编程的需求,这方面还是使用Actor框架好,用消息总线模拟会很累。
    不是一种场景,就算可以使用,也不合适啊。用消息总线的感觉,就有点像用火箭筒打巷战,我们要的是灵活的小***,不是威力强大功能丰富的重型武器,呵呵。

    这是我的代码:http://gist.github.com/150216
    // 你也贴到github上去吧,博客园对于太长代码不太友好,而且有bug……



    1. 你定义了各Message类(如ChatMessage),这就是因为TMessage只能携带一个对象的缘故阿。我的方法就是通过委托轻松地携带了多个参数,其实你也可以用这种方法。就像我说的,我的文章内容适合很多消息传递的场景。

    答: 是的,一个Actor只有TMessage一个对象,其它的参数可以定义到TMessage中,让TMessage帮助携带,当然也可以把委托作为TMessage传递过去

    2. Hook……所以我还是认为难以满足Actor模型中面向并发编程的需求,这方面还是使用Actor框架好,用消息总线模拟会很累。

    答:Hook用处很广的,可以用钩子来记录消息总线的执行步骤,也可以用钩子来进行有选择的接收等等。
    消息总线可以说是对Actor模型的一种扩展,对于有些场景确实Actor框架很方便的,我也准备在消息总线中添加这个Actor模型,来补充消息总线的轻量型,不过我添加的Actor模型也是可以支持并行的,也可以不支持并行的,完全根据项目的需要来定制。

  19. 风云
    *.*.*.*
    链接

    风云 2009-07-20 17:52:00

    Jeffrey Zhao:
    @风云
    selective receive不是那么简单的啊,你给出的erlang代码还是普通的pattern matching而已。
    selective receive是会改变消息执行顺序的,就是有选择的略过一些,放到以后再执行,你给的例子,是并行的match。如这个:

    我们设想有这样一个服务器集群,其中有一台Master,若干台Worker,若干台Client。
    Client 向 Workder 要求他做 sth. 的时候,Worker 要求 Master 返回一些 info,然后Worker 继续完成 sth.

    看下Worker这个服务器:

    loop(...



    1. selective receive是会改变消息执行顺序的,就是有选择的略过一些,放到以后再执行。

    答:消息总线的前端过滤器,钩子以及过滤器都可以改变消息执行顺序的,但是过滤掉就不会被调度器进行再调度了,你的问题问的确是太好了,有点像书签机制,条件不匹配时先挂起,当满足条件是恢复执行,不过目前的消息总线是不支持的,我可以考虑这种情况

  20. 老赵
    admin
    链接

    老赵 2009-07-20 17:56:00

    @风云
    cool,原本我以为selective receive是可有可无的,不过后来我明白这是逻辑控制中非常重要的手段。
    目前搞这方面的人还比较少,希望可以多多交流。

  21. 风云
    *.*.*.*
    链接

    风云 2009-07-20 18:10:00

    老赵:
    明天我写一个基于你ActorLite的扩展,使其支持并行的模式匹配出来,对于像书签机制的模式匹配你看看有什么好办法,我下班了,拜拜!!

  22. 老赵
    admin
    链接

    老赵 2009-07-20 18:15:00

    @风云
    建议不要使用ActorLite,这只是个最简单的试验品,我也正准备放弃呢。
    支持selective receive的api我还没有想好,我打算再仔细考虑一下,用C#实现真是不容易,主要是语法上不知如何保证灵活的功能。

  23. alonesail[未注册用户]
    *.*.*.*
    链接

    alonesail[未注册用户] 2009-07-21 09:03:00

    看来我是远远的被落后了.

  24. vczh[未注册用户]
    *.*.*.*
    链接

    vczh[未注册用户] 2009-07-21 09:44:00

    老赵你怎么就死都不肯用反射呢?假设我要发送的消息一共有type1、type2和type3,那么将函数全部定义成Receive(type1)、Receive(type2)和Receive(type3),初始化的时候用一个hashtable:type->{GetMethod("Receive")里面的index}稍微包装一下,剩下的都无敌简单,而且代码无敌漂亮。何必呢。

  25. 老赵
    admin
    链接

    老赵 2009-07-21 09:50:00

    @vczh
    这样做你是不是要定义大量的message type?这和我说的“强类型消息”一样的缺点。其实你也就是和这种做法一样,只是少了点if else而已,对不对?所以这是“强类型消息”的一个改进方式。
    我使用的是更好的方法了,而且我也觉得我的代码也很简单和漂亮啊,呵呵。而且,我这些文章的是为今后提出的“模式匹配”打一个基础,慢慢的你会知道的。

  26. Galactica
    *.*.*.*
    链接

    Galactica 2009-07-21 12:46:00

    Jeffrey Zhao:
    @Galactica
    哪方面像?



    Actor 和 Actor 通讯,互相发送 Message;
    Service 和 Service 通讯,互相发送 Message;
    Dialog 和 Dialog 通讯,互相发送 Message;

  27. 老赵
    admin
    链接

    老赵 2009-07-21 12:50:00

    @Galactica
    哦,还是不一样的,主要是同步和异步的区别。

  28. Galactica
    *.*.*.*
    链接

    Galactica 2009-07-21 13:27:00

    Jeffrey Zhao:
    @Galactica
    哦,还是不一样的,主要是同步和异步的区别。



    Actor 是 One-Way 的wcf,PostMessage的Dialog。
    wcf 有 Message Dispatcher,Dialog有Message Pump,楼主想实现的应该就是这两个功能。

    wcf使用OperationContract消除switch case,Dialog使用 AFX_MESSAGE_MAP消除switch case,同时又保持了对象行为多态和静态类型安全。

    在不久前的一篇“不够面向对象的面向对象”的文章里,不记得是不是楼主提到过“砍人”的行为是“刀”的,还是“人”的类似的问题。到了楼主这系列文章里,我感觉就有个定论了:“砍人”的行为是“刀”的,但是“刀”要“砍人”,得有一个Actor给“刀”发送一条 Message。

  29. 老赵
    admin
    链接

    老赵 2009-07-21 14:23:00

    @Galactica
    hmmm……似乎有点意思啊……

  30. vczh[未注册用户]
    *.*.*.*
    链接

    vczh[未注册用户] 2009-07-21 16:55:00

    @Jeffrey Zhao
    看了Galactica,我感觉刀不仅可以接受message砍人,而且人也可以用参数提供的刀砍人……

  31. vczh[未注册用户]
    *.*.*.*
    链接

    vczh[未注册用户] 2009-07-21 16:56:00

    @Jeffrey Zhao
    我觉得你写了很多代码给C#去搞这个pattern matching,那么比起F#有没有更好?如果有,是否可以用F#做到比你的东西更好的效果?如果没有,那为什么要做?我认为语言的出现总是为了解决某些具体的问题的。

  32. 老赵
    admin
    链接

    老赵 2009-07-21 17:06:00

    @vczh
    首先,F#的pattern matching对于Actor模式其实不够用(或者说,不够好用),这在(2)里已经描述过了。
    其次,F#过于难学,它的Actor模式基于workflow,虽然灵活、强大且统一,但是要用好,实在不容易。
    我认为F#在相当时间内想要普及,真的很难,它不如Scala在易用性和功能上把握的那么好。
    此外,我没有花很多代码给C#搞pattern matching啊,我只是在列举各种做法,引出我的解决方案,而我的解决方案可以说几乎没有任何额外代码,只是创建接口而已。
    还有就是,我提出这些,是为我接下来要发布的Actor框架作铺垫。语言虽然有其特点和擅长之处,但是我不觉得什么东西都要交给各种不同语言完成。
    例如,说起面向并发编程就是Erlang独有,我不同意。这种编程方式也可以在其他语言和平台中通过框架来实现。
    就算做不到“极致”,做到“够用”也是有价值的,例如,我用C#写Actor,就可以充分利用丰富的框架,也可以让C#程序员可以快速实践面向并发编程。
    这就是价值,不是吗?

  33. vczh[未注册用户]
    *.*.*.*
    链接

    vczh[未注册用户] 2009-07-22 09:14:00

    @Jeffrey Zhao
    为什么我在这里要提起F#而不是Haskell呢,因为F#已经被设计成可以跟.net无缝结合。如果你是给C++写actor那就不会提这个问题了。其实Erlang的存在也证明了functional programming的那种方法并不是非常难学,只是一开始比较别扭而已。所以我认为你将你的东西建立在F#上的价值更大,而且使用你的actor的人也不一定非得用F#,仍然可以用他们熟悉的C#。

  34. vczh[未注册用户]
    *.*.*.*
    链接

    vczh[未注册用户] 2009-07-22 09:15:00

    @Jeffrey Zhao
    当然我并不是指Erlang整个是建立在FP上面的

  35. 老赵
    admin
    链接

    老赵 2009-07-22 09:31:00

    @vczh
    有些东西,用C#更方便,反而没有办法用F#。例如接下去我解决高耦合的问题时会利用到C#的协变/逆变特性,F#没有这个特性,所以做不了。
    不过我最终的Actor模型,应该也是可以在F#下面使用的,因为就是一个.NET类库而已。

    至于为什么我用C#写类库,而不是用F#写类库……
    首先,F#已经有现成的了,只是C#没有办法使用而已,不过F#目前的实现还不够强大。
    其次,如果我for F#写,很可能也会用到F#中的workflow,这样C#就没法使用了。如果不用workflow,那还是用C#写方便。
    最后,就算用F#写框架,其中也会用到了大量的.NET中的类库,最后其实对于F#的特性反而没有用到什么,还是在类C#编程。

    几番考虑之下,我还是决定用C#写,因为C#适用面比F#高,语言特性少,因此为C#设计API通用性可能更广一些。如果需要的话,以后也可以基于C# Actor类库,实现一个适合F#的Adaptor。
    不过,等我什么时候对F#理解更多了,可能看法会有所改变。:)

  36. vczh[未注册用户]
    *.*.*.*
    链接

    vczh[未注册用户] 2009-07-22 10:48:00

    你要是这么说那我也就只好算了……

  37. 老赵
    admin
    链接

    老赵 2009-07-22 11:07:00

    @vczh
    欢迎常来提意见,帮我扩展思路,呵呵。

  38. vczh[未注册用户]
    *.*.*.*
    链接

    vczh[未注册用户] 2009-07-22 17:14:00

    @Jeffrey Zhao
    说道协变/逆变,我觉得F#很奇怪的,Haskell写出来的每一个函数都是泛型函数,你用不同类型的参数去用它它就会特化成不同的东西。F#没了这个好用的特性- -b显得有点力不从心啊。

  39. 老赵
    admin
    链接

    老赵 2009-07-22 17:26:00

    @vczh
    F#也是这样的啊,如果没有显式指定类型,如果可以的话,类型推演会自动当作是范型方法。
    不过这个和协变/逆变无关……

  40. ooops..[未注册用户]
    *.*.*.*
    链接

    ooops..[未注册用户] 2009-07-23 08:46:00

    请问LZ Erlang有好的IDE或者Editor吗?谢谢

  41. 老赵
    admin
    链接

    老赵 2009-07-23 09:50:00

    @ooops..
    我就是用Programming Erlang里介绍的Emacs,事实上我也没有写过几行代码。

  42. vczh[未注册用户]
    *.*.*.*
    链接

    vczh[未注册用户] 2009-07-23 11:33:00

    @Jeffrey Zhao
    其实在做数据处理的程序的时候,逆变/协变改成用pattern matching来搞往往方便得多。

  43. 老赵
    admin
    链接

    老赵 2009-07-23 11:37:00

    @vczh
    不太理解,你是指什么?这两者怎么会产生替代关系?

  44. Laser.NET
    *.*.*.*
    链接

    Laser.NET 2009-09-20 13:32:00

    @Jeffrey Zhao
    呵呵,对erlang的模式匹配的实现原理不是很了解。
    用C#的if ... else 或者 switch之类的形式 在执行效率上 和erlang的模式匹配相比 哪个更好?
    我甚至怀疑那种通用的模式匹配的执行效率反而会更差。。只是从代码的语法上比较简洁和优雅而已。

  45. laorentou
    61.152.150.*
    链接

    laorentou 2015-10-22 20:13:20

    我觉得Actor的核心价值就是可以通过串行的执行来避免共享数据写操作时的各种问题,那么我们利用好这一核心优势就好了,为什么把其他的东西都变成Actor来使用消息传递?例如一个电商系统,对库存的写操作封装到一个Actor里面就好了,这个Actor的调用方没必要也是Actor。 还有很多场景中返回值是需要的,不过有时候是同步的返回还是异步的返回确实很困扰,同步的返回可能会阻塞调用方,异步的返回有时候编程又很复杂。

    @Jeffrey Zhao 您怎么看?

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我