编程中的「消息通信」

在之前的一篇关于「面向对象编程」讨论的文章中,我想着重强调「消息通信」这一概念在原初设计面向对象系统时的重要性。不过,在二零一九年 OO 大行其道的现状下,来理解「消息通信」和面向对象编程之间的关联,确实不容易。那么如何帮助大家在 2019 年理解这一概念呢?我希望在本文中尝试回答。

在编程的上下文讨论消息传递时,一般有两种定义。第一种定义是大众广泛理解的,在并发编程范式中,消息通信是异步分发的一种方法,用于在进程间的通信。不同的进程甚至可能存在于不同的机器上。通常,不同的并发模型可能会有不同角度的考量,如 receiver 需不需要和 sender 在某地汇合,亦或者,receiver 需不需要为 sender 命名以作区分等等。第二种定义则是 Smalltalk 系编程语言中涉及的,非常简单,即对象的方法调用。在 Smalltalk 中,消息是异步的。也就是说,调用者(caller)会等待被调用者(callee)返回结果。

Smalltalk 的消息通信

Smalltalk 使用「消息通信」可以追溯到它的起源。受 Actor 设计思想的启发和影响,最初被设计出来的 Smalltalk 系语言不是过程式的,它没有过程调用的概念,而是 Actor 式消息传送的变种。但这一设计在之后 Smalltalk 的版本演化中被移除了,原因之一是 Actor 式的计算模型在传统工作站下模拟出来的成本非常高。另外就是受当时 Lisp 研究实际在工作站上运行的结果影响。尽管后来彻底演变成了传统的过程式语言(更像是 Lisp 的变体),但是 Actor 模型下的术语被沿用了下来。再后来随着硬件性能的指数级增长,在过程式计算的硬件设备上模拟基于 Actor 模型的计算变得便宜,Alan Kay 开始后悔当初因性能问题而把 Smalltalk 改变为基于过程式计算模型的编程语言。也因此才有了那篇被激烈讨论的邮件

梳理清楚了面向对象语言中「消息通信」这个名词的来源,让我们继续用该名词,详细看下 Smalltalk 中的消息通信。

Smalltalk 中,可以把消息理解成进行计算的请求。当一个对象接收到消息时,它负责响应消息——执行消息所表示的计算。具体来说,该对象根据消息名,在自己的方法命名空间(method namespace)中匹配并执行方法。方法在 Smalltalk 中亦为对象,它本身包含有算法过程(计算可能有副作用)。概念上,方法可类比于函数(function)或者子进程(subroutine),消息类比于函数或者子进程的调用。消息(或方法)的名字是它的选择器(selector),消息(或方法)选择器表达它本身的意图,同时用以区分不同的消息(或方法)。另外,消息可能含有零个或更多的参数。一般地,方法可能接收零个或者更多参数,所以最终这些消息中的参数也就作为方法所接收的参数。方法命名空间(method namespace)维护了一组方法选择器和方法的映射关系,使用选择器可查找对应要执行的方法。动态消息分发(dynamic message dispatch)代指使用方法命名空间找到对应方法的过程。应注意的是,Kay 博士(Alan Kay,Smalltalk 作者)使用方法(method)这一名词指代那些使用运行时动态消息分发方式,因响应消息而被调用的函数或者子线程。而这也是 Smalltalk 中方法和其他语言中函数的本质不同。也正是因为动态消息分发,发送消息给对象完全取决于接收消息的对象。同样的消息发送给不同的对象,可能会被解释为成完全不同的意义。这是因为,不同的对象可能使用不同的方法命名空间通过消息选择器查找方法,同样的消息选择器可能关联不同的方法。也正因此,消息可以被理解为抽象方法调用,即接收消息的对象来选择具体要执行的方法。此时,消息负责为逻辑计算过程命名,接收该消息的对象则负责实际的计算过程。

举个例子来理解下 Smalltalk 中消息传递的概念。假设我们已经有一个 Student 的类和变量 aPerson,Student 类包含一个 name 方法。让我们来看以下语句,

^Student new name: aPerson name

按照消息执行的顺序,以上的声明语句按照如下顺序执行,

  1. 发送消息 new 至 Student 类;
  2. 发送消息 name 至变量 aPerson 所指向的对象;
  3. 发送消息 name: (连同上一步的返回结果,作为参数)至消息 new 的结果,即实例化 Student 的对象;

由于消息 name: 没有重写默认返回值,因此上述声明语句会返回实例化 Student 的对象,也即消息 name: 消息的接收者。

Elixir 的消息通信

和 Erlang 一样,Elixir 基于 Erlang VM,致力于方便地构建可扩展易维护的应用程序。在借用了 Erlang 的优势的前提下,汲取了其他编程语言的精华,以此来提升 Erlang 中缺失的代码组织的能力——编写,分析和修改的能力。

Elixir 的代码运行在 process 中。不同的 process 彼此独立,它们并行运行的同时通过发送消息来互相沟通。需要注意的是,Elixir 中 process 的概念和我们平时所说的操作系统的进程(process) 完全不是一个层级的概念。Elixir 中的进程极其轻量,也因此,我们可以使用 Elixir 轻松启动数以百万计的进程。

我们通过 Elixir 中 send 和 receive 来进一步说明进程间消息传递的方式。send 用于从当前进程向另外某一进程发送消息,

iex> send self(), {:hello, "world"}
{:hello, "world"}

。以上代码中,当前进程给自己发送了一条消息 {:hello, “world”}。然后我们执行以下 receive 代码处理接受到的消息,

iex> receive do
...> {:hello, msg} -> msg
...> {:world, msg} -> "won't match"
...> end
"world"

。仍旧是当前进程,匹配接收的消息以决定如何处理。

Elixir 的每个进程都有属于自己的信箱(process mailbox),一旦有消息被发送至某个进程,该消息则会立即被存储在目标进程的信箱中。目标进程的 receive 块,会遍历信箱,查找匹配给定模式的消息。上例中如模式 :hello。如果信箱中的消息没有匹配到任何给定模式,则当前进程会一直等待,直到信箱中进来模式匹配成功的消息。(此时一个比较常见的处理方式是制定 timeout)另外,发送消息的进程本身不会产生阻塞,一旦它把消息投递到目标进程的信箱中之后,立即开始执行其他任务。

参考文章:

Smalltalk: Getting The Message

Message passing

 

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

You are commenting using your WordPress.com account. Log Out /  更改 )

Google photo

You are commenting using your Google account. Log Out /  更改 )

Twitter picture

You are commenting using your Twitter account. Log Out /  更改 )

Facebook photo

You are commenting using your Facebook account. Log Out /  更改 )

Connecting to %s