PID类型
在之前的语法篇中,我们并没有介绍 PID
这个类型,它和并发息息相关,因此我们在这里来学习它。
PID是进程标识符的意思,用来标识一个erlang进程。在所有相连的erlang节点中,PID都是唯一的。但是PID会被复用,当一个进程终止后,它的PID可以被其他进程再次使用。
进程ID用尖括号包裹的3个点分十进制表示 <x.y.z>
,每个分量是一个十进制数字,一般 x
和z
都是零,如果是一个远端主机上的进程的话,x
就不为0了。
通过 self/0
函数可以获取进程自己的PID,is_pid/1
函数可以用来判断一个值是否是PID。
1> is_pid(1).
false
2> is_pid(<0.58.0>).
true
3> self().
<0.80.0>
4> is_pid(self()).
true
并发
erlang中并发的基本单元是函数,实现并发的函数是 spawn
,它有两种调用方式:
spawn(Mod, Func, Args)
spawn(Fun)
spawn
会在一个新的进程中执行指定的函数,注意这里所说的"进程"并不是操作系统的进程,而是erlang的进程,这种进程非常轻量,因此可以做到非常高的并发量。这两种方式的区别是第一种支持动态代码升级,后者不支持。
span
函数的返回值就是进程PID,通过这个PID我们就可以给该进程发送消息了,这也是erlang中进程间通信的唯一方式。
发送消息
向一个进程发送消息的操作符是 !
,语法为 Pid ! Message
。该表达式的结果任然是 Message
,这就意味着 Pid1 ! Pid2 ! ... ! Message
也是合法的表示式,它会把 Message
发送给 Pid1
、Pid2
等所有进程。
消息是异步发送的,不会阻塞发送进程。
接收消息
接收消息的语法如下:
receive
Pattern1 [when Guard] ->
Expressions1;
Pattern2 [when Guard2] ->
Expressions2;
...
end
receive
是阻塞式的,当消息到达时,会和每个模式进行匹配,并进行关卡验证,验证通过就会执行对应的表达式。receive
只会接收一次消息,如果要循环接收,就需要使用递归。
超时
receive
是阻塞式的,为了防止无限期等待,有时我们需要超时的功能。带超时的接收语法如下:
receive
Pattern1 [when Guard] ->
Expressions1;
Pattern2 [when Guard2] ->
Expressions2;
...
after Time ->
Expressions
end
Time
的单位是毫秒。超时时间有3种类型:
Time=0
,立即超时。Time=有限正数
,正常超时。Time=infinity
,永不超时。
注意这里“等待”的含义并不是“等待消息的到来”,而是等待匹配的消息!这也就是说,即便是进程收到了消息,但是不能匹配接收语句的话,超时还是会执行。如果在超时之前,收到了与接收语句匹配的消息,那么超时语句就不会执行。
receive
语句中可以只包含超时,它会让进程挂起一段时间,比如我们可以用它来实现一个sleep
函数。
sleep(T) ->
receive
after T ->
true
end.
另一点需要注意的是,即便是时间为0的超时,在执行超时语句之前,进程也会先将自己邮箱中的消息与接收语句匹配一遍,如果能够匹配,那么超时也不会执行。我们可以用一个例子来说明这一点。
-module(chaoshi).
-export([test_recv/0]).
test_recv() ->
receive
yes -> io:format("yyds~n")
after 0 ->
test_recv2()
end.
在erlang shell中编译上面的代码,然后执行下面的命令:
2> Pid = spawn(chaoshi, test_recv, []).
<0.116.0>
3> Pid ! yes.
yyds
yes
如果不是先对邮箱中的消息进行匹配,那么yyds
是永远不可能被打印的。
receive的流程
receive
除了做消息匹配还会做消息管理和超时管理,一个典型的接收语句如下:
receive
Pattern1 [when Gruad1] ->
Expression1;
Pattern2 [when Gruad1] ->
Expression2;
...
after
Time ->
ExpressionTimeout
end.
它的工作流程如下:
-
启动一个定时器(如果有
after
的话)。 -
取出进程邮箱的第一条消息,与各个模式匹配,如果匹配成功,将消息从邮箱移除,并执行模式后的表达式。
-
如果第一个消息不能和任何一个模式匹配,系统会将它从进程邮箱移出,放入一个队列保存起来,然后继续尝试第二条消息,重复这一过程直到找到匹配的消息或者邮箱为空。
-
如果所有消息都不匹配,则进程被挂起并重新调度,直到有新消息时到达时,才继续匹配新消息,队列里的消息不会重新匹配。
-
如果新消息匹配成功,队列里的消息会按原顺序重新放入邮箱,如果启动了定时器,会取消定时器。
-
如果定时器先到期了,就会执行
ExpressionTimeout
,并将队列里的消息按原顺序重新放回邮箱。
注册进程
向进程发送消息就需要知道进程的PID,而要记住一个进程的PID是比较难的,而且每次启动进程PID都会变化。
注册进程就是将一个PID和一个原子关联起来,并通过某种方式公布出去,相当于给进程取个名字,这样其他进程通过这个名字就可以给这个进程发送消息了。DNS域名解析以及微服务的服务注册都是相同的套路。
与进程注册有关的函数有4个:
register(Name, Pid)
将Name
(原子类型)与Pid
关联,如果Name
已被注册,此次注册会失败。一旦注册成功,就可以通过Name ! Message
向进程发送消息。unregister(Name)
取消注册,移出与Name
关联的所有注册信息。如果注册进程崩溃,会自动取消注册(这真是太棒了)。whereis(Name)->Pid | undefined
检查Name
是否已被注册,如果是就返回进程PID,否则返回undefined
。registered()->[Name::atom()]
返回系统里所有注册进程的名称列表。