与大多数语言相反,Erlang中的并发很廉价。派生一个进程跟你在普通面向对象语言中分配一个对象的开销差不多。你可能得先好好适应一下,这个理念可真是闻所未闻!但等你适应之后,魔术便开始上演。描绘一组复杂运算,将之切分为若干并发部件,再全部建模为独立的进程。启动运算、派生进程、处理数据,在输出结果后的那一瞬间,所有进程神奇地烟消云散,它们的内部状态、它们持有的数据库句柄、它们打开的套接字,以及一切你不乐意手工清理的东西,都一并消失得无影无踪。
Erlang进程不是操作系统线程。它们由Erlang运行时系统实现,比线程要轻量得多,运行在商用硬件上的单个Erlang系统可以轻易派生出成百上千个进程。运行时系统中所有进程之间相互隔离;单个进程的内存不与其他进程共享,也不会被其他濒死或跑疯的进程破坏
在现代操作系统中,典型的线程会在地址空间中为自己预留数兆的栈空间(也就是说32位的机器上并发线程数最多也就几千个),栈空间溢出便会导致崩溃。另一方面,Erlang进程在启动时栈空间只需要几百字节,并且会自动按需伸缩。
进程被派生并运行起来后还有别的事情要做─它们要进行信息交换。Erlang让通信变得简单。用于消息发送的基本运算符是!,读作“bang”,用法是“目的地!消息”。这就是最简单的消息传递,就像寄明信片一样。OTP框架将进程间通信提升到了另一个层面。,让我们来看看两个独立的并发进程间的通信:
run() ->
Pid = spawn(fun ping/0), Pid ! self(),
receive
pong -> ok
end.
ping() ->
receive
From -> From ! pong
end.
稍微需要注意的地方:调用了spawn的一个变体,参数是一个函数引用,该函数“名为ping、参数数目为零”;再就是函数self( ),它返回当前进程的标识符,用于告知新进程该把消息回复给谁。
这便是Erlang进程通信的概况。每调用spawn一次都会得到一个新的进程标识符,用于唯一标识新创建的子进程。这个标识符后续可用于向子进程发送消息。每个进程都有一个信箱,无论进程繁忙与否,都会先把外来的消息存放在这儿,直到进程下次检查信箱前所有消息都寄存于此。随后进程会在自己乐意的时候用receive表达式从信箱中分检和读取消息,如同示例所示(此处是取走第一条就绪的消息)。
进程完工后,便会消失。它的工作内存、信箱和其他资源都会被回收。该进程若是用作其他进程的数据源,那么它必须在终止前显式地将数据以消息的形式投递出去。
崩溃(异常)造成进程意外提前终止,一旦发生崩溃,其他进程会得到通知。之前我们曾说过进程之间相互独立,单个进程的崩溃不会破坏其他进程,因为它们互不共享内部状态。这构成了Erlang另一主要特性的一个支柱,该特性就是:容错。
?
和许多其他编程语言一样,Erlang也具备异常处理机制来捕获特定代码段的错误,不过它还有一套独一无二的可以有效处理进程故障的进程链接系统,我们在这里进行了解。
Erlang进程意外退出时,会产生一个退出信号。所有与濒死进程链接的进程都会收到这个信号。默认情况下,接收方会一并退出并将信号传播给与它链接的其他进程,直到所有直接或间接链接在一起的进程统统退出为止。这种级联行为可以使一组进程像单个应用一样退出,因此系统整体重启时你不必担心是否还有残存下来未能完全关闭的进程。
前面我们曾提到过利用进程来清理复杂状态。其基本原理是:每个进程完整封装自己的全部状态,因此进程退出时系统的其余部分不会受损。如同单个进程一样,这一点对相互链接的进程组也同样适用。一个进程崩溃,与之协作的其他进程也一并退出,如此便可干净利落地抹掉之前建立的所有复杂状态,既节省了程序员的时间也减少了错误。
OTP实现容错的主要途径之一就是改写退出信号默认的传播行为。通过设置trap_exit进程标记,你可以令进程不再服从外来的退出信号,而是将之捕捉。这种情况下,进程接收到信号后,会先将其转为一条格式为{'EXIT',Pid,Reason}的消息,该消息描述了哪个进程出于什么原因而发生故障,然后这条消息会像普通消息一样被丢入信箱,捕捉到信号的进程就能分检并处理这类消息了。
这类会捕捉信号的进程有时被称为系统进程,它们执行的代码往往有别于普通的工作进程(即通常不捕捉信号的进程)。身为防范退出信号进一步传播的壁垒,系统进程阻断了与之链接的其他进程和外界之间的联系,因而可用于汇报故障乃至重启故障的子系统,
停止并重启整个子系统的目的在于将系统恢复到一个已知的可正常工作的状态。这有点类似于重启电脑:通过重启你可以快刀斩乱麻地将电脑迅速恢复到可工作状态。但重启整台电脑的问题在于粒度太大。理想状况下,应该可以只重启系统的一部分,粒度越小越好。Erlang的进程链接与监督者共同提供了一种细粒度的“重启”机制。
不过,如果就到此为止,你还是得自己从头实现监督机制,这需要缜密的思考和丰富的经验,bug的清除和各种边界情况的处理也要花费大量的时间。幸运的是,OTP框架提供了你所需要的一切:既有运用监督机制来构建应用程序的一套方法,也有稳定的、经过实战考验的基础库。
OTP允许监督者按预设的方式和次序来启动进程。我们还可以告知监督者如何在单个进程故障时重启其他进程、一段时间内尝试重启多少次后放弃重启等。你所要做的就是提供一些参数和回调。
?