部分内容摘抄自《Erlang程序设计第2版》

Erlang的并发概念

Erlang程序反映了我们思考和交流和方式。

  • 我们没有共享“内存”(也就是记忆)。我有我的记忆,你有你的记忆。我们各有一个大脑, 它们并不相连。为了改变你的记忆,我会向你发送一个消息:通过说话,或者挥舞手臂。
  • 你倾听,观察,然后改变了记忆。但是,如果不问你问题或者观察你的反应,我就无法知道你是否收到了我的消息。

这就是Erlang进程的工作方式。Erlang进程没有共享内存,每个进程都有它自己的内存。要改变其他某个进程的内存,必须向它发送一个消息,并祈祷它能收到并理解这个消息。 要确定另一进程收到了你的消息并改变了它的内存,就必须询问它(通过向它发送一条消息)。这就是我们的交流方式。

人类表现为独立的个体,通过发送消息进行交流。

这就是Erlang进程的工作方式,也是我们的工作方式,因此要理解Erlang程序很容易。 一个Erlang程序会包含几十、几千、甚至几十万个小进程。所有这些进程都是独立运作的。

它们通过发送消息来相互交流。每个进程都拥有一块私有内存区域。它们表现得就像是一大群人 在一个巨大的房间里喋喋不休。
这使得Erlang程序天生易于管理和扩展。假设我们有10个人(进程),而他们有太多的工作 要做,我们可以怎么办?找更多的人过来。我们要如何管理这群人?很简单——大声把命令告诉 他们(广播)就可以了。

Erlang进程不共享内存,因此使用内存时无需加锁。有锁的地方就会有钥匙,而钥匙是容易 丢失的。当你丢了钥匙会发生什么?你会惊慌得不知所措。当你在软件系统里丢了钥匙,使锁出 现问题时也会如此。
分布式软件系统里只要有锁和钥匙,就总会出问题。
Erlang没有锁,也没有钥匙。

如果有人死亡,其他人会注意到。

如果我在一个房间里突然倒下死去,很可能就会有人注意到(好吧,至少我希望如此)。Erlang进程就像人类一样,有时会死去。但和人类不同的是,当它们死亡时,会用尽最后一口气喊出导 致它们死亡的准确原因。

想象一个挤满人的房间里突然有一个人倒下死去。就在那一刻,他说“我的心脏病发作了” 或者“我吃得太多,胃爆炸了”。Erlang进程就是这么做的。一个进程可能会在临死时说:“我是 因为有人要求我除以零而死的。”另一个可能会说:“我是因为有人问我空列表的最后一个元素是什么而死的。”

现在,在这个挤满人的房间里,我们可以设想有些人被特别指派从事清理尸体的工作。让我们假设有简和约翰两个人。如果简死了,约翰会处理一切与简的死亡有关的问题。如果约翰死了, 简会处理这些问题。简和约翰通过一种不可见的约定连接在一起,这个约定是如果其中一人死亡, 另一人就会处理一切由此产生的问题。

Erlang的错误检测正是使用的这种方式。进程可以相互连接。如果其中一个进程挂了,另一 个进程就会得到一个说明前者死亡原因的错误消息。

Erlang程序就是这么工作的。
到目前为止,我们学到的以下内容。

  • Erlang程序由大量进程组成。这些进程间能相互发送消息。
  • 这些消息也许能被其他进程收到和理解,也许不能。如果想知道某个消息是否已被对方进程收到和理解,就必须向该进程发送一个消息并等待回复。
  • 进程可以成对相互连接。如果某一对互连进程的其中一个挂了,另一个进程就会收到一个说明前者死亡原因的消息。 这个简单的编程模型是一个大模型的一部分,我把这个大模型称为面向并发编程(concurrency-oriented programming)。

可以联想一下房间里的人。人就是进程。房间里的人都有他们
的私人记忆,进程也是如此。要改变你的记忆,就需要我说给你听。这就是发送和接收消息。我 们有了小孩,这就是分裂(spawn)。我们死了,就是进程退出。

了解顺序Erlang后,编写并发程序就很简单了。只需要三个新的基本函数:spawn、send和 receive。spawn创建一个并行进程,send向某个进程发送消息,receive则是接收消息。

Erlang的并发是基于进程(process)的。进程是一些独立的小型虚拟机,可以执行Erlang 函数。
你肯定曾经接触过进程,但仅仅是在操作系统的上下文环境里。在Erlang里,进程隶属于编 程语言,而非操作系统。这就意味着Erlang的进程在任何操作系统上都会具有相同的逻辑行为, 这样,就能编写可移植的并发代码,让它在任何支持Erlang的操作系统上运行。

你肯定曾经接触过进程,但仅仅是在操作系统的上下文环境里。在Erlang里,进程隶属于编 程语言,而非操作系统。这就意味着Erlang的进程在任何操作系统上都会具有相同的逻辑行为, 这样,就能编写可移植的并发代码,让它在任何支持Erlang的操作系统上运行。
在Erlang里:

  • 创建和销毁进程是非常快速的;
  • 在进程间发送消息是非常快速的;
  • 进程在所有操作系统上都具有相同的行为方式;
  • 可以拥有大量进程;
  • 进程不共享任何内存,是完全独立的;
  • 进程唯一的交互方式就是消息传递。 出于这些原因,Erlang有时会被称为是一种纯消息传递式语言。 如果你没有进程编程的经验,可能听说过它很有难度的传言。你多半听过一些恐怖故事,涉及内存冲突、竞争状况、共享内存破坏,等等。但在Erlang里,进程编程是很简单的。

Erlang有一种公布进程标识符的方法,它让系统里的任何进程都能与该进程通信。这样的进 程被称为注册进程(registered process)。管理注册进程的内置函数有四个

register(AnAtom,Pid)

用AnAtom(一个原子)作为名称来注册进程Pid。如果AnAtom已被用于注册某个进程, 这次注册就会失败。

unregister(AnAtom)

移除与AnAtom关联的所有注册信息
注意 如果某个注册进程崩溃了,就会自动取消注册。

whereis(AnAtom)->Pid undefined

检查AnAtom是否已被注册。如果是就返回进程标识符Pid,如果没有找到与AnAtom关联 的进程就返回原子undefined。

registered()->[AnAtom::atom()]

返回一个包含系统里所有注册进程的列表。

并发函数

Pid = spawn(Mod, Fun, Args)

创建一个新的并发进程来执行apply(Mod, Func, Args)。这个新进程和调用进程并列运 行。spawn返回一个Pid(process identifier的简称,即进程标识符)。可以用Pid来给此进 13 程发送消息。请注意,元数为length(Args)的Func函数必须从Mod模块导出。 当一个新进程被创建后,会使用最新版的代码定义模块。

Pid = spwn(fun)

创建一个新的并发进程来执行Fun()。这种形式的spawn总是使用被执行fun的当前值,而 且这个fun无需从模块里导出。 这两种spawn形式的本质区别与动态代码升级有关。12.8节会讨论如何从这两种spawn形 式中做出选择

Pid!Message

向标识符为Pid的进程发送消息Message。消息发送是异步的。发送方并不等待,而是会 继续之前的工作。!被称为发送操作符。
Pid ! M被定义为M。因此,Pid1 ! Pid2 !…! Msg的意思是把消息Msg发送给Pid1、 Pid2等所有进程。

receive…end

接收发送给某个进程的消息。它的语法如下:

 receive
Pattern1 [when Guard1] -> Expression1;
Pattern2 [when Guard2] -> Expression2;
...
after Interval -> Expression0
end

当某个消息到达进程后,系统会尝试将它与Pattern1(以及可选的关卡Guard1)匹配, 如果成功就执行Expressions1。如果第一个模式不匹配,就会尝试Pattern2,以此类推。 如果没有匹配的模式,消息就会被保存起来供以后处理,进程则会开始等待下一条消息。 12.5节中会介绍更多的细节。 接收语句里的模式和关卡和我们定义函数时使用的模式和关卡具有相同的语法形式和 含义。

好了,就是这些。不需要线程、锁、信号和人工控制。
到目前为止,我们粗略介绍了spawn、send和receive的工作方式。当spawn命令被执行时, 系统会创建一个新的进程。每个进程都带有一个邮箱,这个邮箱是和进程同步创建的。
给某个进程发送消息后,消息会被放入该进程的邮箱。只有当程序执行一条接收语句时才会 读取邮箱。

错误处理的术语含义:

  • 进程
    进程有两种:普通进程和系统进程。spawn创建的是普通进程。普通进程可以通过执行内 置函数process_flag(trap_exit, true)变成系统进程。
  • 连接
    进程可以互相连接。如果A和B两个进程有连接,而A出于某种原因终止了,就会向B发送 一个错误信号,反之亦然。
  • 连接组
    进程P的连接组是指与P相连的一组进程。
  • 监视
    监视和连接很相似,但它是单向的。如果A监视B,而B出于某种原因终止了,就会向A发 送一个“宕机”消息,但反过来就不行了。
  • 消息和错误信号
    进程协作的方式是交换消息或错误信号。消息是通过基本函数send发送的,错误信号则 是进程崩溃或进程终止时自动发送的。错误信号会发送给终止进程的连接组。
  • 错误信号的接收
    当系统进程收到错误信号时,该信号会被转换成{‘EXIT’, Pid, Why}形式的消息。Pid 是终止进程的标识,Why是终止原因(有时候被称为退出原因)。如果进程是无错误终止, Why就会是原子normal,否则Why会是错误的描述。当普通进程收到错误信号时,如果退出原因不是normal,该进程就会终止。当它终止时, 同样会向它的连接组广播一个退出信号。
  • 显式错误信号 任何执行exit(Why)的进程都会终止(如果代码不是在catch或try的范围内执行的话), 并向它的连接组广播一个带有原因Why的退出信号。进程可以通过执行exit(Pid, Why)来发送一个“虚假”的错误信号。在这种情况下,Pid会 收到一个带有原因Why的退出信号。调用exit/2的进程则不会终止(这是有意如此的)。
  • 不可捕捉的退出信号
    系统进程收到摧毁信号(kill signal)时会终止。摧毁信号是通过调用exit(Pid, kill) 生成的。这种信号会绕过常规的错误信号处理机制,不会被转换成消息。摧毁信号只应该 用在其他错误处理机制无法终止的顽固进程上。

如果P1调用link(P3),P1和P3之间就会建立连接。

同步终止的进程组 ,要么同时存在要么同时终止

有时候我们不希望相连的进程全部终止,而是想让系统里的错误停止扩散,要实现这一点,P3可以执行process_flag(trap_exit, true)并转变成一个系统进程(意 思是它可以捕捉退出信号)。

监视与连接类似,但是有几处明显的区别。

  • 监视是单向的。如果被监视的进程挂了,就会向监视进程发送一个“宕机”消息,而不是退出信号。
  • 这就意味着监视进程即使不是系统进程也能够处理错误。

基本错误处理函数

下列基本函数被用来操作连接和监视,以及捕捉和发送退出信号: 
它们的行为类似于spawn(Fun)和spawn(Mod,Func,Args),同时还会在父子进程之间创 建连接。

-spec spawn monitor(Fun)->{Pid,Ref}

它与spawn_link相似,但创建的是监视而非连接。Pid是新创建进程的进程标识符,Ref 是该进程的引用。如果这个进程因为Why的原因终止了,消息 {‘DOWN’,Ref,process,Pid,Why}就会被发往父进程。

-spec process_flag(trap_exit, true)

它会把当前进程转变成系统进程。系统进程是一种能接收和处理错误信号的进程。

它会创建一个与进程Pid的连接。连接是双向的。如果进程A执行了link(B),就会与B相 连。实际效果就和B执行link(A)一样。 如果进程Pid不存在,就会抛出一个noproc退出异常。 如果执行link(B)时A已经连接了B(或者相反),这个调用就会被忽略。

它会移除当前进程和进程Pid之间的所有连接。

-spec erlang:monitor(process,Item)->Ref

它会设立一个监视。Item可以是进程的Pid,也可以是它的注册名称。

-spec demonitor(Ref)->true

它会移除以Ref作为引用的监视。

-spec exit(Why)->none()

它会使当前进程因为Why的原因终止。如果执行这一语句的子句不在catch语句的范围内, 此进程就会向当前连接的所有进程广播一个带有参数Why的退出信号。它还会向所有监视 它的进程广播一个DOWN消息。

-spec exit(Pid,Why)->true

它会向进程Pid发送一个带有原因Why的退出信号。执行这个内置函数的进程本身不会终 止。它可以用于伪造退出信号。 可以用这些基本函数来设立互相监视的进程网络,并把它作为构建容错式软件的起点。


, , ,