为了让几十个任务能同时执行, Erlang 采用了 Actor 模型, 每个 actor 都是虚拟机中的一个独立进程. 在 Erlang 中, 并发的基本单位是进程(Actor). 每个进程代表一个持续的活动, 是某段程序代码的执行代理, 与其他按各自的节奏执行自身代码的进程一起并发运行, 进程之间靠消息来通信. Erlang 的并发是很廉价的, 派生一个进程就跟在 OOP 中分配一个对象的开销差不多. 更形象一些, 如果你是 Erlang 世界中的一个 actor, 你将会是一个孤独的人, 独自坐在一个没有窗户的黑屋子里, 在你的邮箱旁等待着消息. 当你收到一条消息时, 会用特定的方式来响应这条消息:收到账单就要进行支付;收到生日卡, 就回一封感谢信;对于不理解的消息, 就完全忽略. 人和人之间只能通过写信进行交流, 就是这样.


理解进程之前先要说的是 pid 这个特殊的 Erlang 数据类型, Erlang 支持进程编程, 任何代码都需要一个进程作为载体才能执行, 每个进程都有一个唯一标识符, 也即是 pid. 在 erlang shell 中, 进程的 pid 会以类似<0.62.0>的格式打印, 这个格式只用于调试比较目的.

self() 会告诉你当前进程(也就是调用 self() 的那个进程)的 pid.


PROCESS OPERATIONS
派生进程

派生进程的函数有两个: spawn/1, spawn/3. 第一个仅有一个参数, 就是用作新进程入口的 fun 函数, 这里要注意的是, 我们无法得到函数F的返回值. 我们只能得到它的 pid, 因为进程不会返回任何东西; 另一个则需要模块名, 函数名, 参数列表三个参数.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
%% Erlang/OTP 20 [erts-9.0]

%% Eshell V9.0  (abort with ^G)
1> F = fun() -> 2 + 2 end.
#Fun<erl_eval.20.99386804>
2> spawn(F).
<0.63.0>
3> spawn(io, format, ["Hello World!~n"]).
Hello World!
<0.68.0>
消息传递

Erlang 的进程之间可以互相使用 ! 运算符发送消息.

该操作符的左边是一个 pid, 右边可以是任意 Erlang 数据项. 这个数据项会被发送给左边的 pid 所代表的进程, 这个进程就可以访问它了.

1
2
3
%% 给当前进程发送一个 hello 原子
1> self() ! hello.
hello

发送一些无人会看的消息的用处就和写一些表达自我情绪的诗一样(换句话说, 就是不大有用), 呵呵哒.

因此, 我们还需要 receive 表达式来接收消息.

receive 的语法形式如下:

1
2
3
4
5
receive
     Pattern1 when Guard1 -> Expr1;
     Pattern2 when Guard2 -> Expr2;
     Pattern3 -> Expr3
end

举个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
-module(dolphins).
-compile(export_all).

dolphin1() ->
    receive
        do_a_flip ->
            io:format("How about no?~n");
        fish ->
            io:format("So long and thanks for all the fish!~n");
        _ ->
            io:format("Heh, we're smarter than you humans.~n")
    end.
1
2
3
4
5
6
7
8
9
1> c(dolphins).
{ok,dolphins}
2> Dolphin = spawn(dolphins, dolphin1, []).
< 0.39.0>
3> Dolphin ! "oh, hello dolphin!".
Heh, we're smarter than you humans.
"oh, hello dolphin!"
4> Dolphin ! fish
fish

函数执行到了 receive 表达式. 因为进程邮箱为空, 所以海豚进程会一直处于等待消息状态.

收到了消息”oh, hello dolphin!“. 函数会先对 do_aflip 进行模式匹配. 失败了. 然后再尝试模式fish, 也失败了. 最后遇到了匹配一切的子句(), 匹配成功.

进程打印消息”Heh, we’re smarter than you humans.“, 接着, 进程也随之结束. 因此, 再次发送 fishDolphin 的时候, 就不会对该消息做任何反应.


超时接收

上面提到的, 当进程执行到 receive 表达式以后会一直处于等待消息的状态, 而且如果不做处理就会一直等下去. 出现这样情况的原因有很多, 比如准备发送消息的进程在消息发出之前就崩溃掉了. 为了避免超时问题的出现, 可以在 receive 增加超时设置, 如下:

1
2
3
4
5
receive
    Match -> Expression1
after Delay ->
    Expression2
end.

等待 Delay 毫秒以后如果没有 Match 的消息就会执行 after 中的内容, 在这里就是 Expression2


注册进程

每个 Erlang 系统都有一个本地进程注册表用于注册进程的简单命名服务. 一个名称一次只能用于一个进程. 可以使用函数 erlang:register(Name, Pid) 为进程命名. 如果进程死亡了, 它会自动失去自己的名字. 也可以使用函数unregister/1手工解除进程的名字注册. 可以调用 registered/0 得到所有已注册进程的列表, 或者通过shell命令 regs() 得到更详细的信息. 使用内置函数 whereis/1 可以查找当前与指定注册名对应的 pid.

假设某个进程崩溃了, 对应的服务被重启, 新的服务进程 pid 将会改变, 此时无需逐个通知给系统中的所有进程, 只要更新进程注册表就可以了.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
1> registered().
[application_controller,user,standard_error,erl_prim_loader,
 inet_db,init,erts_code_purger,rex,error_logger,user_drv,
 kernel_sup,global_name_server,erl_signal_server,
 standard_error_sup,file_server_2,global_group,
 kernel_safe_sup,code_server]
2> regs().

** Registered procs on node nonode@nohost **
Name                  Pid          Initial Call                      Reds Msgs
application_controlle <0.33.0>     erlang:apply/2                     551    0
code_server           <0.38.0>     erlang:apply/2                  121988    0
erl_prim_loader       <0.6.0>      erlang:apply/2                  139520    0
erl_signal_server     <0.47.0>     gen_event:init_it/6                 51    0
error_logger          <0.32.0>     gen_event:init_it/6                322    0
erts_code_purger      <0.1.0>      erts_code_purger:start/0             4    0
file_server_2         <0.46.0>     file_server:init/1                 113    0
global_group          <0.45.0>     global_group:init/1                 74    0
global_name_server    <0.41.0>     global:init/1                       63    0
inet_db               <0.44.0>     inet_db:init/1                     348    0
init                  <0.0.0>      otp_ring0:start/2                  818    0
kernel_safe_sup       <0.55.0>     supervisor:kernel/1                 83    0
kernel_sup            <0.37.0>     supervisor:kernel/1               2162    0
rex                   <0.40.0>     rpc:init/1                          32    0
standard_error        <0.49.0>     erlang:apply/2                      11    0
standard_error_sup    <0.48.0>     supervisor_bridge:standar           50    0
user                  <0.52.0>     group:server/3                      87    0
user_drv              <0.51.0>     user_drv:server/2                 2703    0

** Registered ports on node nonode@nohost **
Name                  Id              Command
ok
4> whereis(user).
<0.52.0>

链接(link)是两个进程之间的一种特殊关系. 当在两个进程间建立了这种关系后, 如果其中一个进程由于意外的抛出, 出错或者退出而死亡时, 另外一个进程也会死亡, 把这两个进程独立的生存期绑定成一个关联在一起的生存期.

Erlang 中有一个原生函数 link/1, 用于在两个进程间建立一条链接, 它的参数是进程的pid. 当调用它时, 会在当前进程和参数pid标识的进程之间建立一条链接. 要去除链接, 可以使用 unlink/1.

当链接进程中的一个死亡时, 会发送一条特殊的消息, 其中含有死亡原因相关的信息. 如果进程正常死亡了(函数正常执行完毕), 就不会发送这条消息.

注意, link(spawn(Function)) 或者 link(spawn(M,F,A)) 并不是一个原子操作. 有时, 进程会在链接建立成功之前死亡, 从而导致不期望的行为. 因此, Erlang 中增加了 spawn_link/1spawn_link/3 函数, 对应 spawn/1spawn/3, 创建一个进程, 并和它建立链接, 这个操作是原子级的, 也就意味着要么成功, 要么失败.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
-module(linkmon).
-export([myproc/0]).
myproc() ->
    timer:sleep(5000),
    exit(reason).

1> self().
<0.60.0>
2> c(linkmon).
{ok,linkmon}
3> spawn_link(fun linkmon:myproc/0).
<0.68.0>
** exception error: reason
4> self().
<0.70.0>
监控器 Monitor

有时候链接可能不是你想要的, 也许只是想知道目标进程挂了而不想牵连着一起被杀死, 那么这时候就要用监控器(Monitor). 监控器其实是一个单向的链接, 可以让一个进程在不影响目标进程的情况下对目标进程进行监视.

创建监控器的函数是 erlang:monitor/2, 它的第一个参数永远是原子 process, 第二个参数是进程的 Pid.

1
2
3
4
5
6
1> erlang:monitor(process, spawn(fun() -> timer:sleep(500) end)).
#Ref<0.3856117142.833355780.165079>
2> flush().
Shell got {'DOWN',#Ref<0.3856117142.833355780.165079>,process,<0.62.0>,normal}
ok
3>

每当被监控的进程死亡时, 监控进程都会收到一条消息, 格式为 {'DOWN', MonitorReference, process, Pid, Reason}

其中, MonitorReference 可以用来解除对一个进程的监控. 记住, 监控器是可叠加的, 因此会收到多条 DOWN 消息. 引用可以唯一确定一条 DOWN 消息. 类似 spawn_link 的也有原子级的 spawn_monitor