顺畅的使用C# Actor:另一个解决方案

开发 后端
Actor模型中的对象如果要进行交互,唯一的手段便是发送消息。本文为C# Actor提供了另一个解决方案,但这个方案有着一些致命的缺陷。

在前两篇文章中,我们了解到Erlang中灵活的模式匹配,以及在C#甚至F#中会都遭遇的尴尬局面。那么现在就应该来设计一个解决方案了,我们如何才能在C#这样的语言里顺畅地使用Actor模型呢?不仅如此,***我们还能获得其它一些优势。

C# Actor:“消息”、“协议”和“接口”
Actor模型中的对象如果要进行交互,唯一的手段便是发送消息。不同语言/平台上的消息有不同的表现形式,但是它们所传递的信息是一致的:
◆做什么事情
◆做这件事情需要的数据

例如,Erlang中往往会使用Tag Message的格式作为消息:

  1. {doSomething, Arg1, Arg2, Arg3, ...} 

其中,原子doSomthing表示“做什么”,而后面的ArgN便是一个个的参数,使用Erlang中的模式匹配可以很方便地捕获消息中的数据。在C#等语言中,由于并非专为了Actor模型设计,因此一个Message往往只能是一个对象。但是这个对象的职责并没有减轻,因此我们需要自己处理的事情就多了。我们可能会这样做:

学Erlang的Tag Message,但是这样会产生大量丑陋的类型转换操作,并且丧失了静态检查功能。
为每种消息创建不同的Message类型,但是这样会产生大量类类型,每个类型又有各种属性,非常麻烦。
这两种做法在上一篇文章里都有过讨论,感兴趣的朋友可以再去“回味”一番。那么,究竟什么是消息呢?根据我的理解,“消息”其实是这么一种东西:
◆“消息”表示“发送方”和“接受方”之间的“通信协议”(例如Erlang中的“模式”)。
◆“消息”表示“发送方”要“接受方”所做的事情,但是并没有要求“接受方”需要怎么做。
◆一个Actor可能会会作为“接受方”遵守多种“通信协议”。

经过这样的描述,您是否觉得.NET中有一种东西和“消息”非常接近?没错,那就是“接口”,因为:
◆“接口”从概念上讲便是一种“协议”。
◆“接口”表示“能做什么”,但没有限制“怎么做”。
◆一个Actor可以实现多个接口,即遵守多种协议。

看上去还真是一一对应啊!那么我们再来深入一步进行对比,“接口”能否传递消息所要表现的信息?答案也是肯定的:
◆做什么事情:接口中的一个方法。
◆需要的数据:接口的参数。

也就是说,如之前的那条Erlang消息,在C#中便可以表示为:

  1. x.DoSomething(arg1, arg2, arg3, ...) 

基于这样的类比,我们发现使用“接口”还可以带来一个额外的东西,那就是“消息组”。如Erlang这样语言,消息与消息之间是完全独立的。.NET中的接口可以包含多个方法,这就是一种“分组”,我们可以利用这种方式来更好地管理有关联的消息。此外,利用.NET中的访问限制符(public,internal等)还可以实现消息的公开和隐藏。而且因为接口的参数是强类型的,所以可以得到编译期的检查,也可以享受编辑工具的代码提示及重构……C#编程里的种种优势似乎我们一个都没有拉下。

C# Actor看似美好的实现
等一下,接口只是一种“协议”,但是“消息”还必须是一个实体,一个对象,并且“携带”了这个协议才能在Actor之间传递埃这个对象除了携带协议所需要的数据以外,还要能够告诉接受方究竟该“操作什么”。“操作”带上“数据”,于是我就想到了“委托”。例如,如果我们想要发送一个“协议”,叫做IDoHandler,那么我们便可以构造一个Action﹤IDoHandler>对象——这正是Lambda表达式的用武之地:

  1. Action﹤IDoHandler> m = x => x.Do(0, 1, 2, ...); 

好,那么我们还是用乒乓测试来尝试一番。我们知道,乒乓测试会让Ping对象和Pong对象相互发送消息,我们各使用一个“消息组”,也就是“接口”来定义消息:

  1. public interface IPongMessageHandler { }  
  2.  
  3. public interface IPingMessageHandler { }  

那么,Ping和Pong两个Actor类型又该如何定义呢?我们知道,Ping需要处理Pong发来的消息,因此它需要实现IPongMessageHandler接口,并且需要接受类型为Action﹤IPongMessageHandler>的消息。Pong与Ping类似,因此它们的定义为:

  1. public class Ping : Actor﹤Action﹤IPongMessageHandler>>, IPongMessageHandler  
  2. {  
  3.     private int m_count;  
  4.  
  5.     public Ping(int count)  
  6.     {  
  7.         this.m_count = count;  
  8.     }  
  9.  
  10.     protected override void Receive(Action﹤IPongMessageHandler> message)  
  11.     {  
  12.         message(this);  
  13.     }  
  14.  
  15.     ...  
  16. }  
  17.  
  18. public class Pong : Actor﹤Action﹤IPingMessageHandler>>, IPingMessageHandler  
  19. {  
  20.     protected override void Receive(Action﹤IPingMessageHandler> message)  
  21.     {  
  22.         message(this);  
  23.     }  
  24.  
  25.     ...  
  26. }  

从代码上看,实际操作中我们并不需要让Ping或Pong直接继承Handler接口,只要最终提供一个对象给message执行即可。严格说来,“接口”只是一个“消息组”,具体的“消息”还是要落实到接口中的方法。定义了Ping和Pong之后,我们便可以明确接口中的方法了(确切地说,是明确了方法的参数):

  1. public interface IPongMessageHandler  
  2. {  
  3.     void Pong(Pong pong);  
  4. }  
  5.  
  6. public interface IPingMessageHandler  
  7. {  
  8.     void Ping(Ping ping);  
  9.     void Finish();  
  10. }  

使用了接口,自然就要提供方法的实现了。我们先从典型而简单的Pong对象看起:

  1. public class Pong : Actor﹤Action﹤IPingMessageHandler>>, IPingMessageHandler  
  2. {  
  3.     ...  
  4.  
  5.     #region IPingMessageHandler Members  
  6.  
  7.     void IPingMessageHandler.Ping(Ping ping)  
  8.     {  
  9.         Console.WriteLine("Pong received ping");  
  10.         ping.Post(h => h.Pong(this));  
  11.     }  
  12.  
  13.     void IPingMessageHandler.Finish()  
  14.     {  
  15.         Console.WriteLine("Finished");  
  16.         this.Exit();  
  17.     }  
  18.  
  19.     #endregion  
  20. }  

原本需要在得到消息之后,根据消息的内容作出不同的响应。而现在,消息会被自动转发为接口中的方法调用,我们只需要实现特定的方法即可。在Ping方法中,我们会得到一个Ping类型的对象——于是我们再向它回复一个消息。消息的类型是Action﹤IPongMessageHandler>,可以看出,使用Lambda表达式构造这样一个消息特别方便。

Ping类也只需要实现IPongMessageHandler即可,只是这段逻辑“略显复杂”:

  1. public class Ping : Actor﹤Action﹤IPongMessageHandler>>, IPongMessageHandler  
  2. {  
  3.     ...  
  4.  
  5.     public void Start(Pong pong)  
  6.     {  
  7.         pong.Post(h => h.Ping(this));  
  8.     }  
  9.  
  10.     #region IPongMessageHandler Members  
  11.  
  12.     void IPongMessageHandler.Pong(Pong pong)  
  13.     {  
  14.         Console.WriteLine("Ping received pong");  
  15.  
  16.         if (--this.m_count > 0)  
  17.         {  
  18.             pong.Post(h => h.Ping(this));  
  19.         }  
  20.         else 
  21.         {  
  22.             pong.Post(h => h.Finish());  
  23.             this.Exit();  
  24.         }  
  25.     }  
  26.  
  27.     #endregion  
  28. }  

收到Pong消息之后,将count减1,如果还大于0,则回复一个Ping消息,否则就回复一个Finish并退出。***启动乒乓测试:

new Ping(5).Start(new Pong());由于使用了接口作为消息的协议,因此无论是编辑器还是编译器都可以给我们足够的支持。同时,对于消息的处理也无须如上一篇文章那样不断进行判断和类型转换,代码可谓流畅不少。

C# Actor致命的缺陷
虽说没有***的东西,但目前的缺陷却是致命的。

在实际使用过程中,消息的“发送方”和消息的“接收方”应该完全无关,它们互不知道对方具体是谁,只应该基于“协议”,也就是“接口”来实现。可惜在上面这段代码中,很多东西都被“强横”地限制住了。例如,Ping消息会附带一个ping对象作为参数,ping对象会等待一个Pong消息。但是,发送Ping消息(并等待Pong消息)的一方很可能是各种类型的Actor,不一定是Ping类型。有朋友可能会说,那么我们把IPingMessageHandler的Ping方法的签名改成这样,不就可以了吗?

  1. void Ping(Actor﹤Action﹤IPongMessageHandler>> ping) 

是的,此时的ping,的确是在“等待Pong消息的Actor对象”。但是,这意味着ping对象它也只能是这个指明的Actor类型了。在实际使用过程中,这几乎是不可能的事情。因为一个Actor很可能会接受各种消息,它很难做到“一心一意”。因此这篇文章所提出的做法,几乎只能满足如乒乓测试这样简单的Actor模型使用场景。我们必须改变。

改变的方式有不少,从“向弱类型妥协”到“利用.NET 4.0中的协变/逆变”,都可以满足不同的场景——不过我们还是下次再说吧。

F#的实现
本文描述的方式也可以运用在在F#中。首先自然还是接口的定义:

  1. type IPingMessageHandler =  
  2.     abstract Ping : Ping -> unit  
  3.     abstract Finish : unit -> unit  
  4.  
  5. and IPongMessageHandler =   
  6.     abstract Pong : Pong -> unit  

以上便是F#中定义接口的方式,与C#相比更为简洁。接着便是Ping类型的实现:

  1. and Ping() =  
  2.     inherit (IPongMessageHandler -> unit) Actor()  
  3.     let mutable count = 5  
  4.     override self.Receive(message) = message self  
  5.  
  6.     member self.Start(pong : Pong) =   
  7.         pong ﹤﹤ fun h -> self |> h.Ping  
  8.       
  9.     interface IPongMessageHandler with   
  10.         member self.Pong(pong) =  
  11.             printfn "Ping received pong" 
  12.             count ﹤- count - 1  
  13.             if (count > 0) then  
  14.                 pong ﹤﹤ fun h -> self |> h.Ping  
  15.             else 
  16.                 pong ﹤﹤ fun h -> h.Finish()  
  17.                 self.Exit()Pong类型的实现则更为简单:  
  18.  
  19. and Pong() =  
  20.     inherit (IPingMessageHandler -> unit) Actor()  
  21.     override self.Receive(message) = message self  
  22.       
  23.     interface IPingMessageHandler with  
  24.         member self.Ping(ping) =  
  25.             printfn "Pong received ping" 
  26.             ping ﹤﹤ fun h -> self |> h.Pong  
  27.           
  28.         member self.Finish() =  
  29.             printfn "Finished" 
  30.             self.Exit()启动乒乓测试:  
  31.  
  32. (new Pong()) |> (new Ping()).Start;  
  33.  

【编辑推荐】

  1. C# Actor的尴尬与F#美丽外表下的遗憾
  2. 看Erlang中Actor模型的执行方式和优劣
  3. Erlang面向分布与并发的编程语言
  4. Erlang十分钟快速入门
  5. 因并发而生 因云计算而热:Erlang专家访谈实录
责任编辑:yangsai 来源: 老赵点滴
相关推荐

2009-08-14 00:55:21

C#程序编译

2016-12-26 15:23:21

戴尔

2009-08-26 18:17:26

C#基于Object解

2011-03-28 14:02:07

MirahJava对手

2012-01-12 10:09:55

Elementary 思路

2012-03-21 11:10:00

JDBCMySQLJava

2009-03-17 09:27:52

ITSMITIL解决方案

2021-05-29 07:13:26

微软Nobelium网络攻击

2024-01-15 00:35:23

JavaScript框架HTML

2011-11-10 09:46:41

云计算云管理

2018-11-05 11:06:38

openmediavaNAS 方案

2012-03-02 09:22:11

程序员

2018-12-05 09:00:46

DevOps持续交付持续集成

2021-06-16 12:03:49

WindowsLinux游戏

2023-02-10 15:41:50

物联网物联网平台

2009-07-28 11:37:55

7类布线端口

2011-11-14 09:41:10

Linux Mint

2019-07-08 10:01:33

物联网IOT技术

2010-11-30 16:50:42

2011-11-15 10:16:04

Linux操作系统
点赞
收藏

51CTO技术栈公众号