使用Erlang而不是其余函数式编程语言的主要缘由之一就是Erlang的并发处理能力和分布式编程。并发意味着程序能够在同一时刻执行多个线程。举个例子,操做系统容许你在同一时刻运行文字处理程序,电子表格程序,邮件客户端,和打印任务。系统中的每一个处理器(CPU)有可能只处理一个线程,可是它以必定频率交换这些线程,给咱们形成一种多个程序是在同一时刻执行的假象。在一个Erlang程序中很容易建立并行执行(parallel execution)的线程,而且运行这些这些线程互相通讯。Erlang中,每一个执行线程称之为进程(process)。html
(旁白:术语“进程(process)”一般用于各个执行线程不共享数据,术语‘’线程(thread)”用于当它们以某种方式共享数据。Erlang执行线程不共享数据,这就是为何它们叫作进程的缘由)node
Erlang内置函数spawn用于建立一个新进程:spawn(Module, Exported_Function, List of Arguments)。考虑下面的模块shell
-module(tut14). -export([start/0, say_something/2]). say_something(What, 0) -> done; say_something(What, Times) -> io:format("~p~n", [What]), say_something(What, Times - 1). start() -> spawn(tut14, say_something, [hello, 3]), spawn(tut14, say_something, [goodbye, 3]).
5> c(tut14). {ok,tut14} 6> tut14:say_something(hello, 3). hello hello hello done
如上所示,函数say_something输出第一个参数,输出次数由第二个参数指定。函数start启动两个进程,一个输出“hello”三次,一个输出“goodbye”三次。每一个进程都使用say_something函数。注意用spawn这种方式启动一个进程所用到的函数,必须从该模块导出。(即写在模块开头的-export里面)编程
9> tut14:start(). hello goodbye <0.63.0> hello goodbye hello goodbye
注意它没有先输出三次“hello”再输出三次“goodbye”。相反,第一个进程输出“hello”,第二个进程输出“goodbye”,而后第一个进程再输出“hello”,如此继续。可是<0.63.0>从哪里来?一个函数的返回值是最后一行表达式的返回值。在start中最后一个表达式是服务器
spawn(tut14, say_something, [goodbye, 3]).
spawn返回一个进程标识符(process identifier) , 或者说pid, 标明独一无二的进程。因此<0.63.0>是上面spawn函数调用返回的pid。下一个例子展现了怎么使用pid。cookie
同时还要注意在io:format中用~p代替~w。引用手册的话:“~p和~w以相同的方式输出标准语,可是若是输出表示的项比一行长会合理的折断成多行。它也尝试去检测一个可输出的字符列表并将至以字符串的形式输出。”并发
(译注:这里举个例子(数据来源于官方),在shell中输入:app
4> F = [{attributes,[[{id,age,1.50000},{mode,explicit},{typename,"INTEGER"}], [{id,cho},{mode,explicit},{typename,'Cho'}]]}, {typename,'Person'},{tag,{'PRIVATE',3}},{mode,implicit}]. 5> io:format("~p",[F]). [{attributes,[[{id,age,1.5},{mode,explicit},{typename,"INTEGER"}], [{id,cho},{mode,explicit},{typename,'Cho'}]]}, {typename,'Person'}, {tag,{'PRIVATE',3}}, {mode,implicit}]ok 6> io:format("~w",[F]). [{attributes,[[{id,age,1.5},{mode,explicit},{typename,[73,78,84,69,71,69,82]}],[{id,cho},{mode,explicit},{typename,'Cho'}]]},{typename,'Person'},{tag,{'PRIVATE',3}},{mode,implicit}]ok
)编程语言
在接下来的例子中建立了两个进程,它们互相发送一些消息。分布式
-module(tut15). -export([start/0, ping/2, pong/0]). ping(0, Pong_PID) -> Pong_PID ! finished, io:format("ping finished~n", []); ping(N, Pong_PID) -> Pong_PID ! {ping, self()}, receive pong -> io:format("Ping received pong~n", []) end, ping(N - 1, Pong_PID). pong() -> receive finished -> io:format("Pong finished~n", []); {ping, Ping_PID} -> io:format("Pong received ping~n", []), Ping_PID ! pong, pong() end. start() -> Pong_PID = spawn(tut15, pong, []), spawn(tut15, ping, [3, Pong_PID]). 1> c(tut15). {ok,tut15} 2> tut15: start(). <0.36.0> Pong received ping Ping received pong Pong received ping Ping received pong Pong received ping Ping received pong ping finished Pong finished
函数start建立了一个进程,让咱们把它叫作“pong”:
Pong_PID = spawn(tut15, pong, [])
这个进程执行tut15:pong()。Pong_PID是pong进程的进程标识符。接着建立一个名为“ping”的进程:
spawn(tut15, ping, [3, Pong_PID]),
这个进程执行:
tut15:ping(3, Pong_PID)
<0.36.0>是start函数的返回值。
“pong”进程如今这样:
receive finished -> io:format("Pong finished~n", []); {ping, Ping_PID} -> io:format("Pong received ping~n", []), Ping_PID ! pong, pong() end.
receive 结构用于使进程等待另外一个进程的消息。它有下面的格式:
receive pattern1 -> actions1; pattern2 -> actions2; .... patternN actionsN end.
注意在end.前面没有“;”
Erlang进程之间传递的消息简单的被认为是有效的erlang项(term)。也便是说,它们能够是列表,tuple,整数,原子,pid等等。
每一个进程有它本身的消息队列,用于接收消息。当新消息到达时会放入队列的尾部。当一个进程执行一个receive表达式,消息队列第一个接收到的消息(头部)会和receive结构进行模式匹配。若是匹配成功,消息将会移出队列而且执行模式后面指定的action
然而,若是第一个模式没有匹配,第二个模式将会继续,若是成功就执行它对应的action,若是没有成功,继续匹配第三个模式,如此继续。若是到最后都没有模式匹配成功,第一个消息将会保留在消息队列,而后消息队列的第二个消息(头部下一个)继续进行匹配,若是有任何一个模式匹配成功,相应的action就会执行,而后第二个消息会移出队列(除第二个之外的消息全都保留)。若是第二个消息没有匹配,尝试第三个,如此继续。直到到达消息队列尾部。若是到达队列尾部,进程会阻塞(中止执行)并等待一个新消息到达,而后重复上述过程。
Erlang的实现是很机智的,在每一个receive中它会尽量的最小化每一个消息的模式匹配次数。
如今回到ping pong的例子。
"Pong"等待消息。若是接收到原子finished,“pong”就会输出“Pong finished”,而后什么也不作,终止。若是收到一个{ping,Ping_PID}格式的消息,它会输出"Pong received ping" 并向“ping”进程发送一个原子pong消息:
Ping_PID ! pong
注意“!”运算符是如何发送消息的。“!”的语法是:
Pid ! Message
即将消息(任何Erlang项)发送到Pid表示的进程。
在向“ping”进程发送了pong消息后,“pong”函数会调用自身,致使它从新回到receive结构等待另外一条消息。
如今让咱们看看“ping”进程。回忆一下它是这样开始的:
tut15:ping(3, Pong_PID)
请看函数ping/2,由于第一个参数是3(不是0)(第一个clause是 ping(0,Pong_PID),第二个clause是ping(N,Pong_PID),因此N成为3),因此ping/2的第二个clause被执行。
第二个clause向pong进程发送一条消息:
Pong_PID ! {ping, self()},
self()返回执行self()的进程的pid,在这个是“ping”进程的pid。(回忆一下“pong”的代码,self()的值最终会到达以前所说的receive结构中的Ping_PID变量。)
如今"Ping"等待一个来自“pong”的答复:
receive pong -> io:format("Ping received pong~n", []) end,
当收到回复时它会输出"Ping received pong",在这以后ping函数也会调用本身。
ping(N - 1, Pong_PID)
N-1使得第一个参数减一,直到它变成零。 当变成零时,ping/2的第一个clause就会被执行:
ping(0, Pong_PID) -> Pong_PID ! finished, io:format("ping finished~n", []);
该函数会向pong进程发送原子finished(正如上面描述的这会使得pong结束进程),接着会输 "ping finished"。 而后"Ping"会由于没有事情作而终止。
在以前的例子中,“pong”进程最早被建立,并将它的进程标识符给接下来建立的“ping”进程做为参数。也便是说,“ping”必须经过某种方式知道“pong”进程才能向它发送消息。有时独立启动的进程须要知道彼此的标识符。鉴于此Erlang提供了一种进程机制来给进程命名而不是在一堆函数中混乱传递PID参数,这种机制是经过内置函数register完成的。
register(some_atom, Pid)
如今让咱们使用下面的代码来重写ping pong 例子,给“pong”进程一个名字:
-module(tut16). -export([start/0, ping/1, pong/0]). ping(0) -> pong ! finished, io:format("ping finished~n", []); ping(N) -> pong ! {ping, self()}, receive pong -> io:format("Ping received pong~n", []) end, ping(N - 1). pong() -> receive finished -> io:format("Pong finished~n", []); {ping, Ping_PID} -> io:format("Pong received ping~n", []), Ping_PID ! pong, pong() end. start() -> register(pong, spawn(tut16, pong, [])), spawn(tut16, ping, [3]).
2> c(tut16). {ok, tut16} 3> tut16:start(). <0.38.0> Pong received ping Ping received pong Pong received ping Ping received pong Pong received ping Ping received pong ping finished Pong finished
这是start/0函数,
register(pong, spawn(tut16, pong, [])),
同时作了启动“pong”线程,给线程命名两件事。在“ping”进程中,能够这样给“pong”进程发送消息:
pong ! {ping, self()},
ping/2 如今变成了ping/1,省去了Pong_PID参数(避免在各个函数中混乱传递Ping_PID/Pong_PID参数)
让咱们重写ping pong这个例子,使“ping”和“pong”在不一样电脑上运行。第一件事是设置。Erlang的分布式实现提供了一个很是基础的验证机制来避免一台电脑不当心链接到Erlang分布式集群。Erlang集群的交流必须有一个相同的magic cookie。要实现这个最简单的方法是经过一个.erlang.cookie文件,将它放置于集群中的各台电脑(译注:即服务器,后文也译作“电脑(computer)”)的home目录,这样它们就能相互通讯:
.erlang.cookie文件包含了一行相同的原子。举个例子,在Linux或UNIX系统shell中
$ cd $ cat > .erlang.cookie this_is_very_secret $ chmod 400 .erlang.cookie
chmod命令将只容许文件的拥有者访问.erlang.cookie文件。这是需求不是必要。
当你启动一个Erlang系统,想和另外一个Erlang系统通讯,你必须给它一个名字,好比:
$ erl -sname my_name
在后面咱们会讨论更多关于这个的细节。若是你想实验一下分布式Erlang,可是你只有一台电脑,你能够在这台电脑上启动两个独立的Erlang系统,只须要给它们指定不一样的名字。每一个运行着Erlang系统的电脑叫作Erlang节点(Erlang node)
(注意: erl -sname假定全部节点都是用相同的IP,若是咱们想在不一样的IP上运行Erlang系统请使用 -name代替。可是IP地址必须给全。)
像下面同样修改ping pong例子使之运行在不一样的节点:
-module(tut17). -export([start_ping/1, start_pong/0, ping/2, pong/0]). ping(0, Pong_Node) -> {pong, Pong_Node} ! finished, io:format("ping finished~n", []); ping(N, Pong_Node) -> {pong, Pong_Node} ! {ping, self()}, receive pong -> io:format("Ping received pong~n", []) end, ping(N - 1, Pong_Node). pong() -> receive finished -> io:format("Pong finished~n", []); {ping, Ping_PID} -> io:format("Pong received ping~n", []), Ping_PID ! pong, pong() end. start_pong() -> register(pong, spawn(tut17, pong, [])). start_ping(Pong_Node) -> spawn(tut17, ping, [3, Pong_Node]).
假设这两台电脑叫作gollum和kosken。第一个节点是kosken,启动ping,第二个是gollum,启动pong。
kosken以下:
kosken> erl -sname ping Erlang (BEAM) emulator version 5.2.3.7 [hipe] [threads:0] Eshell V5.2.3.7 (abort with ^G) (ping@kosken)1>
这是 gollum:
gollum> erl -sname pong Erlang (BEAM) emulator version 5.2.3.7 [hipe] [threads:0] Eshell V5.2.3.7 (abort with ^G) (pong@gollum)1>
接着在gollum上启动pong:
(pong@gollum)1> tut17:start_pong(). true
在kosken节点上启动ping进程:
(ping@kosken)1> tut17:start_ping(pong@gollum). <0.37.0> Ping received pong Ping received pong Ping received pong ping finished
如上所示,ping pong都已经在运行了。在“pong”那边:
(pong@gollum)2> Pong received ping Pong received ping Pong received ping Pong finished (pong@gollum)2>
注意tut17的代码,你会注意到pong函数的代码没有改变,下面的代码也同样,它不关心ping进程所在的节点:
{ping, Ping_PID} -> io:format("Pong received ping~n", []), Ping_PID ! pong,
所以,Erlang pid包含了进程在哪执行的信息。若是你知道一个进程的pid,就能够用“!”运算符发送消息,而不用考虑进程在不在相同的节点。
有一点不一样是消息怎样发送给另外一个节点上已注册的进程:
{pong, Pong_Node} ! {ping, self()},
一个元组tuple {registered_name,node_name}用来代替 registered_name。
在钱的例子中,‘’ping”和“pong”由两个独立的Erlang节点的shell中启动,也就是说spawn能够在不一样的节点上启动进程。
下面的例子又是ping pong程序,可是这一次“ping”在另外一个节点启动:
-module(tut18). -export([start/1, ping/2, pong/0]). ping(0, Pong_Node) -> {pong, Pong_Node} ! finished, io:format("ping finished~n", []); ping(N, Pong_Node) -> {pong, Pong_Node} ! {ping, self()}, receive pong -> io:format("Ping received pong~n", []) end, ping(N - 1, Pong_Node). pong() -> receive finished -> io:format("Pong finished~n", []); {ping, Ping_PID} -> io:format("Pong received ping~n", []), Ping_PID ! pong, pong() end. start(Ping_Node) -> register(pong, spawn(tut18, pong, [])), spawn(Ping_Node, tut18, ping, [3, node()]).
假设在kosken上被名为ping的Erlang系统已经启动,而后在gollum上这样作:
(pong@gollum)1> tut18:start(ping@kosken). <3934.39.0> Pong received ping Ping received pong Pong received ping Ping received pong Pong received ping Ping received pong Pong finished ping finished
注意gollum接收全部的输出。这是由于I/O系统会找到进程从哪启动,而后在那输出。
如今写一个完整的例子,叫作“messenger”。messenger这个程序运行在不一样的Erlang节点上登录而后互相发送消息(message)。
在开始前,注意下面几点:
messenger容许建立客户端而后链接中央服务器,并服务器会知晓客户端是哪些、它们在哪。也就是说,用户不须要关系当前节点的名字和其余节点在哪就能发送消息。
messenger.erl文件以下:
%%% Message passing utility. %%% User interface: %%% logon(Name) %%% One user at a time can log in from each Erlang node in the %%% system messenger: and choose a suitable Name. If the Name %%% is already logged in at another node or if someone else is %%% already logged in at the same node, login will be rejected %%% with a suitable error message. %%% logoff() %%% Logs off anybody at that node %%% message(ToName, Message) %%% sends Message to ToName. Error messages if the user of this %%% function is not logged on or if ToName is not logged on at %%% any node. %%% %%% One node in the network of Erlang nodes runs a server which maintains %%% data about the logged on users. The server is registered as "messenger" %%% Each node where there is a user logged on runs a client process registered %%% as "mess_client" %%% %%% Protocol between the client processes and the server %%% ---------------------------------------------------- %%% %%% To server: {ClientPid, logon, UserName} %%% Reply {messenger, stop, user_exists_at_other_node} stops the client %%% Reply {messenger, logged_on} logon was successful %%% %%% To server: {ClientPid, logoff} %%% Reply: {messenger, logged_off} %%% %%% To server: {ClientPid, logoff} %%% Reply: no reply %%% %%% To server: {ClientPid, message_to, ToName, Message} send a message %%% Reply: {messenger, stop, you_are_not_logged_on} stops the client %%% Reply: {messenger, receiver_not_found} no user with this name logged on %%% Reply: {messenger, sent} Message has been sent (but no guarantee) %%% %%% To client: {message_from, Name, Message}, %%% %%% Protocol between the "commands" and the client %%% ---------------------------------------------- %%% %%% Started: messenger:client(Server_Node, Name) %%% To client: logoff %%% To client: {message_to, ToName, Message} %%% %%% Configuration: change the server_node() function to return the %%% name of the node where the messenger server runs -module(messenger). -export([start_server/0, server/1, logon/1, logoff/0, message/2, client/2]). %%% Change the function below to return the name of the node where the %%% messenger server runs server_node() -> messenger@bill. %%% This is the server process for the "messenger" %%% the user list has the format [{ClientPid1, Name1},{ClientPid22, Name2},...] server(User_List) -> receive {From, logon, Name} -> New_User_List = server_logon(From, Name, User_List), server(New_User_List); {From, logoff} -> New_User_List = server_logoff(From, User_List), server(New_User_List); {From, message_to, To, Message} -> server_transfer(From, To, Message, User_List), io:format("list is now: ~p~n", [User_List]), server(User_List) end. %%% Start the server start_server() -> register(messenger, spawn(messenger, server, [[]])). %%% Server adds a new user to the user list server_logon(From, Name, User_List) -> %% check if logged on anywhere else case lists:keymember(Name, 2, User_List) of true -> From ! {messenger, stop, user_exists_at_other_node}, %reject logon User_List; false -> From ! {messenger, logged_on}, [{From, Name} | User_List] %add user to the list end. %%% Server deletes a user from the user list server_logoff(From, User_List) -> lists:keydelete(From, 1, User_List). %%% Server transfers a message between user server_transfer(From, To, Message, User_List) -> %% check that the user is logged on and who he is case lists:keysearch(From, 1, User_List) of false -> From ! {messenger, stop, you_are_not_logged_on}; {value, {From, Name}} -> server_transfer(From, Name, To, Message, User_List) end. %%% If the user exists, send the message server_transfer(From, Name, To, Message, User_List) -> %% Find the receiver and send the message case lists:keysearch(To, 2, User_List) of false -> From ! {messenger, receiver_not_found}; {value, {ToPid, To}} -> ToPid ! {message_from, Name, Message}, From ! {messenger, sent} end. %%% User Commands logon(Name) -> case whereis(mess_client) of undefined -> register(mess_client, spawn(messenger, client, [server_node(), Name])); _ -> already_logged_on end. logoff() -> mess_client ! logoff. message(ToName, Message) -> case whereis(mess_client) of % Test if the client is running undefined -> not_logged_on; _ -> mess_client ! {message_to, ToName, Message}, ok end. %%% The client process which runs on each server node client(Server_Node, Name) -> {messenger, Server_Node} ! {self(), logon, Name}, await_result(), client(Server_Node). client(Server_Node) -> receive logoff -> {messenger, Server_Node} ! {self(), logoff}, exit(normal); {message_to, ToName, Message} -> {messenger, Server_Node} ! {self(), message_to, ToName, Message}, await_result(); {message_from, FromName, Message} -> io:format("Message from ~p: ~p~n", [FromName, Message]) end, client(Server_Node). %%% wait for a response from the server await_result() -> receive {messenger, stop, Why} -> % Stop the client io:format("~p~n", [Why]), exit(normal); {messenger, What} -> % Normal response io:format("~p~n", [What]) end.
要运行这个程序,你须要:
接下来的例子是使用这个程序,在四个不一样电脑上启动Erlang节点。若是你没有那么多电脑那么能够考虑在一台机器上启动不一样的节点(译注:-sname,具体能够参见前面小结)。
四个Erlang节点分别是:messenger@super, c1@bilbo, c2@kosken, c3@gollum.
首先启动服务器节点messenger@supe:
(messenger@super)1> messenger:start_server(). true
接着在c1@bilbo上登录Peter:
(c1@bilbo)1> messenger:logon(peter). true logged_on
在c2@kosken上登录James:
(c2@kosken)1> messenger:logon(james). true logged_on
Fred在c3@gollum上登录:
(c3@gollum)1> messenger:logon(fred). true logged_on
如今Peter给Fred发送消息:
(c1@bilbo)2> messenger:message(fred, "hello").
ok
sent
Fred收到消息并回复Peter一条消息而后注销:
Message from peter: "hello" (c3@gollum)2> messenger:message(peter, "go away, I'm busy"). ok sent (c3@gollum)3> messenger:logoff(). logoff
James如今尝试向Fred发送消息:
(c2@kosken)2> messenger:message(fred, "peter doesn't like you").
ok
receiver_not_found
可是失败了,由于Fred早就离线了。
让咱们先看看这里引进的新概念。
有两个版本的server_transfer函数:一个有四个参数(server_transfer/4) 一个有五个参数(server_transfer/5)。Erlang将他们视做不一样的函数。
注意怎样写server函数让它调用本身,经过server(User_List)造成一个循环结构。Erlang编译器很“聪明”,它会进行代码优化,以致于它真的会变成一个循环而不是函数调用。可是这只限于在这个调用后没有其它工做。这会致使进程(译注:的内存占用)在每次循环后变得愈来愈大。
也使用了一些lists模块的函数。这是一个很是有用的模块,建议看看它的使用手册(erl -man lists)。lists:keymember(Key,Position,Lists)遍历tuple列表而后检查tuple的Position位置是否和Key匹配,tuple的第一个元素是1.若是寻找成功返回true,不然返回false。
3> lists:keymember(a, 2, [{x,y,z},{b,b,b},{b,a,c},{q,r,s}]). true 4> lists:keymember(p, 2, [{x,y,z},{b,b,b},{b,a,c},{q,r,s}]). false
lists:keydelete的工做方式相似,只是若是找到就删除它并返回剩余列表:
5> lists:keydelete(a, 2, [{x,y,z},{b,b,b},{b,a,c},{q,r,s}]).
[{x,y,z},{b,b,b},{q,r,s}]
lists:keysearch相似于lists:keymember,可是返回 {value,Tuple_Found},或者寻找失败返回false原子。
在lists模块有不少有用的函数。
一个Erlang进程(概念上的)会一直运行直到它执行receive结构,直到遍历消息队列后没有发现和receive结构中的模式相匹配的消息。之因此说是“概念上的”是由于Erlang系统执行各个进程实际上是会共享CPU时间的。
当一个进程没有事作的时候它会终止,即它调用的最后一个函数简单返回且再也不调用其余函数。另外一个终止进程的方法是调用exit/1,。exit/1的参数有特别的意义,咱们将会在后面讨论。在这个例子中,调用exit(normal)便可,它会进程运行到没有事作再终止是同样的效果。
内置函数whereis(RegisteredName)检查一个名为RegisteredName的具名进程是否存在。若是存在,返回它的pid,若是不存在, ,返回原子undefined。
到目前为止你应该已经理解了messenger模块的大部分代码。让咱们取一个片断看看它的细节。
第一个用户“发送”消息:
messenger:message(fred, "hello")
在测试了客户端进程存在以后:
whereis(mess_client)
将会发送一条消息给mess_client:
mess_client ! {message_to, fred, "hello"}
它的实现是客户端向服务器发送消息:
{messenger, messenger@super} ! {self(), message_to, fred, "hello"},
而后等待服务器的回复。
把目光转向服务器,它收到消息而后调用:
server_transfer(From, fred, "hello", User_List),
它检查User_List中的pid:
lists:keysearch(From, 1, User_List)
若是keysearch返回原子false,引起错误,服务器会这样回复:
From ! {messenger, stop, you_are_not_logged_on}
它将被客户端收到,而后客户端执行exit(normal)终止。若是keysearch返回{value,{From,Name}},很明显用户已经登陆,他的名字(peter)会被绑定到Name上。
如今让咱们调用:
server_transfer(From, peter, fred, "hello", User_List)
注意server_transfer/5,它不一样于server_transfer/4。另外一个keysearch会在User_List上进行,而后返回fred客户端的pid:
lists:keysearch(fred, 2, User_List)
此次Position指定为2,也就是tuple的第二个元素和fred进行匹配。若是返回原子false,fred就没有登陆而后发送下面的消息:
From ! {messenger, receiver_not_found};
客户端会收到该条消息。
若是keysearch返回:
{value, {ToPid, fred}}
会向fred发送:
ToPid ! {message_from, peter, "hello"},
向peter发送:
From ! {messenger, sent}
Fred'收到该条消息而后输出:
{message_from, peter, "hello"} ->
io:format("Message from ~p: ~p~n", [peter, "hello"])
Peter客户端在await_result函数调用中接收消息。