并发概念其实早已根植于我们的大脑,根本无需后天学习,它是本能的一部分。人类对于刺激的反应很快,这个过程是由大脑中一个叫做杏仁核的部分所掌控。如若没有这种应激反应,很难想象我们人类如何能够生存到今天。对于生存来说,有意识的思考速度太慢,很多时候,在我们意识到需要“踩刹车”之前,我们的身体已经先做出了反应。
在主干道上开车时,我们的大脑同时跟踪着数十辆乃至数百辆汽车的位置。这个过程是下意识的,并不需要我们有意识的参与。试想,如果没有这种能力,开车会是一件多么危险的事情。
在Erlang里,进程属于程序语言而非操作系统。
在Erlang里:
创建和销毁进程非常迅速;
在两个进程间收发消息非常迅捷;
进程在所有的操作系统上行为相同;可以创建大量进程;
进程之间不共享任何数据,彼此完全独立;
进程间交互的唯一方法就是通过消息传递。
由于这些特性,Erlang有时也被称作纯粹的消息传递语言。
Pid = spawn(Fun)
创建一个新的并发进程,用于对Fun求值。新进程与调用者所在的进程并发运行。spawn返回一个Pid[process identifier(进程标识符)的缩写]。你可以使用Pid向进程发送消息。
Pid ! Message
向标识符为Pid的进程发送消息。消息发送是异步的,发送者无须等待返回结果就可以继续处理它自己的事务。!被称为发送操作符。
-module(area_server).
-export([loop/0]).
loop()->
receive
{rectangle,Width,Ht} ->
io:format( "Area of rectangle is ~p~n",[Width * Ht]),
loop();
{circle,R}->
io:format( "Area of circle is ~p~n",[3.14159 * R * R]),
loop();
Other ->
io:format( "I don't know what the area of a ~p is ~n" , [Other]),
loop()
end.
我们能够在shell中创建一个进程对loop/0进行求值:
5> Pid = spawn(fun area_server:loop/0).
<0.71.0>
上面的代码说明了什么?在第1行,创建了一个新的并发进程。spawn(Fun)创建了一个并发进程来对函数Fun求值,它返回Pid,这里就是打印出来的<0.71.0>。
6> Pid!{rectangle,6,10}.
Area of rectangle is 60
{rectangle,6,10}
7> Pid!{circle,23}.
Area of circle is 1661.90111
{circle,23}
8> Pid!{triangle,2,4,5}.
I don't know what the area of a {triangle,2,4,5} is
{triangle,2,4,5}
第二行向这个进程发送了消息。这个消息与1oop/0中的receive语句的第一个模式匹配:
一旦接收到这个消息,进程就会打印出矩形的面积。之所以会在shell中打印{rectangle,6,10},这是因为Pid! Msg的值就被定义为Msg。如果我们向进程发送一个不能识别的消息,那么它会打印警告。也就是在receive语句中通过0ther->. . .代码进行处理。
在客户/服务器架构下的客户机和服务器都是分离的进程,Erlang的消息传递则被用于客户机和服务器之间的通信。客户机和服务器可以运行在同一台机器上,也可以运行在不同的机器上。
客户和服务器这两个词涉及分别由两个进程扮演的不同角色。客户机总是通过向服务器发送请求来发起一个计算,服务器则在计算出一个结果之后向客户端发送一个回应。
下面就来写第一个客户/服务器应用程序。
-module(area_server_final).
-export([start/0,area/2]).
start() -> spawn( fun loop/0).
area(Pid,What) ->
rpc(Pid,What).
rpc(Pid,Request) ->
Pid ! {self(),Request},
receive
{Pid,Response} ->
Response
end.
loop() ->
receive
{From,{rectangle,Width,Ht}} ->
From ! {self(),Width * Ht},
loop();
{From,{circle,R}} ->
From ! {self(),3.14159 * R * R},
loop();
{From,Other} ->
From ! {self(),{error,Other}},
loop()
end.
注意,我们是如何向由From参数标识的进程发送计算结果的。因为客户机已经在参数中表明了自己的进程ID,所以客户机就会收到这个结果。
.发送起始请求的进程被称为客户机。接收请求然后发送回应的进程就叫做服务器,
rpc[remote procedure call(远程过程调用)的缩写],它用来封装发送请求和等待回应;
要运行它,我们可以调用函数start/0和area/2(也就是我们调用spawn和rpc的入口)。这两个名字恰到好处地描述了服务器的行为:
5> Pid = area_server_final:start().
<0.67.0>
6> area_server_final:area(Pid,{rectangle,10,8}).
80
7> area_server_final:area(Pid,{circle,4}).
50.26544
为了探寻究竟,要对大量Erlang进程的创建时间进行统计。下面就是进行统计的代码:
-module(processes).
-export([max/1]).
max(N) ->
Max = erlang : system_info(process_limit),
io:format( "Maximum a17owed processes:~p~n",[Max]),
statistics(runtime),
statistics(wall_clock),
L = for(1,N, fun() -> spawn(fun() -> wait() end) end),
{_,Time1} = statistics(runtime),
{_,Time2} = statistics(wall_clock),
lists :foreach(fun(Pid) -> Pid ! die end,L),
U1 = Time1 * 1000 / N,
U2= Time2 * 1000 / N,
io:format( "Process spawn time=~p (~p) microseconds~n",[U1,U2]).
wait() ->
receive
die -> void
end.
for(N,N,F) ->[F()];
for(I,N,F) ->[F() |for(I+1,N,F)].
15> c(processes).
{ok,processes}
16> processes:max(20000).
Maximum a17owed processes:262144
Process spawn time=1.55 (1.55) microseconds
ok
创建20000个进程时,每过进程平均花费1.55us的cpu时间,一共花费了1.55us的真实时间
如果想要向一个进程发送消息,那么就需要知道它的PID。不过这种方法通常很麻烦,因为这意味着系统中所有发送消息的进程都知道这个PID。另一个方面,这种做法在安全上也有问题,如果你不公开一个进程的PID,那么其他的进程就根本无法与它通信。
Erlang有一种机制可以用于发布一个进程的标识符以便其他进程可以与之通信,这种进程就叫做注册进程。
erlang中有四个BIF用于管理注册进程
register(AnAtom,Pid)
将一个进程Pid注册一个名为AnAtom的原子,如果原子AnAtom已经被另一个注册进程所使用,那么注册就会失败。
unregister(AnAtom)
移除与AnAtom相对应进程的所有注册信息。
whereis(AnAtom) -> Pid | undefined
判断AnAtom是否已经被其他进程注册。如果成功,则返回进程标识符Pid。如果AnAtom没有与之相对应的进程,那么就返回原子undefined。
registered() -> [AnAtom::atom()]
返回一个系统中所有已经注册的名称列表。
-module(processes).
-compile(export_all).
start() ->
spawn(fun() -> loop([]) end).
rpc(Pid,Request) ->
Pid ! {self(),Request},
receive
{Pid,Request} ->
Request
end.
loop(X) ->
receive
Any ->
io:format("Receive:~p~n",[Any]),
loop(X)
end.
接收循环仅是一个空循环,它会将接收到的所有消息都打印出来。
编程时,首先会先向进程发送消息。因为程序启动的接收循环可以匹配所有的消息,因此能得到一些由最底层的receive语句打印出来的信息。看到这些结果之后,我们会向接收循环增加匹配模式,然后继续测试。这种技术在很大程度上决定了编码的顺序。
在编写的第一个程序中
loop() ->
receive
{From,{rectangle,Width,Ht}} ->
From ! {self(),Width * Ht},
loop();
{From,{circle,R}} ->
From ! {self(),3.14159 * R * R},
loop();
{From,Other} ->
From ! {self(),{error,Other}},
loop()
end.
每接收一个消息,就会处理这个消息,然后立刻再次调用loop(),这个过程就被称作尾递归。编译尾递归的函数可以使在一系列语句最后的一个函数调用可以被替换为一个简单的跳转指令,指向被调用函数的开头。这就意味着一个尾递归的函数可以无限循环而不需要消耗栈空间。
在绝大多数的程序中,我们都用spawn(Fun)来创建一个新进程。这在我们无须动态地更新代码时能够很好的工作。但有时我们还想要编写那些可以在运行时更新的代码。如果想要确保代码可以被很好地进行动态更新,那么就必须使用不同形式的spawn。
spawn(Mod,FuncName,Args)
它会创建一个新的进程。Args是一个形如[Arg1,Args2,...,ArgN]的参数列表。新进程会从函数Mod :FuncName(Arg1,Arg2,...,ArgN)开始执行。
MFA:(M 模块,F 函数名 ,A 参数列表)