Erlang Concurrent 并发进阶

2018-08-31 11:06:11 浏览数 (1)

写在前面的话

  • 本文来源于官方教程 Erlang -- Concurrent Programming。虽然没有逻辑上的关系,但建议在掌握了Erlang入门系列教程的一些前置知识后继续阅读。
  • 之前我是逐小结翻译然后发布,写到后面感觉并发这篇文章连贯性较强,分开不便于阅读,所以现在把它合并为一篇文章,刚好赶在开学前完成,也算对自己一个交待。希望能读得开心,如有错误恳请指正。

1. 进程

使用Erlang而不是其他函数式编程语言的主要原因之一就是Erlang的并发处理能力和分布式编程。并发意味着程序可以在同一时刻执行多个线程。举个例子,操作系统允许你在同一时刻运行文字处理程序,电子表格程序,邮件客户端,和打印任务。系统中的每个处理器(CPU)有可能只处理一个线程,但是它以一定频率交换这些线程,给我们造成一种多个程序是在同一时刻执行的假象。在一个Erlang程序中很容易创建并行执行(parallel execution)的线程,并且运行这些这些线程互相通信。Erlang中,每个执行线程称之为进程(process)。

(旁白:术语“进程(process)”通常用于各个执行线程不共享数据,术语‘’线程(thread)”用于当它们以某种方式共享数据。Erlang执行线程不共享数据,这就是为什么它们叫做进程的原因)

Erlang内置函数spawn用于创建一个新进程:spawn(Module, Exported_Function, List of Arguments)。考虑下面的模块

代码语言:javascript复制
-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]).
代码语言:javascript复制
5> c(tut14).
{ok,tut14}
6> tut14:say_something(hello, 3).
hello
hello
hello
done

如上所示,函数say_something输出第一个参数,输出次数由第二个参数指定。函数start启动两个进程,一个输出“hello”三次,一个输出“goodbye”三次。每个进程都使用say_something函数。注意用spawn这种方式启动一个进程所用到的函数,必须从该模块导出。(即写在模块开头的-export里面)

代码语言:javascript复制
9> tut14:start().
hello
goodbye
<0.63.0>
hello
goodbye
hello
goodbye

注意它没有先输出三次“hello”再输出三次“goodbye”。相反,第一个进程输出“hello”,第二个进程输出“goodbye”,然后第一个进程再输出“hello”,如此继续。但是<0.63.0>从哪里来?一个函数的返回值是最后一行表达式的返回值。在start中最后一个表达式是

代码语言:javascript复制
spawn(tut14, say_something, [goodbye, 3]).

spawn返回一个进程标识符(process identifier) , 或者说pid, 标明独一无二的进程。所以<0.63.0>是上面spawn函数调用返回的pid。下一个例子展示了怎么使用pid。

同时还要注意在io:format中用~p代替~w。引用手册的话:“~p和~w以相同的方式输出标准语,但是如果输出表示的项比一行长会合理的折断成多行。它也尝试去检测一个可输出的字符列表并将至以字符串的形式输出。”

(译注:这里举个例子(数据来源于官方),在shell中输入:

代码语言:javascript复制
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

2. 消息传递

在接下来的例子中创建了两个进程,它们互相发送一些消息。

代码语言:javascript复制
-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”:

代码语言:javascript复制
 Pong_PID = spawn(tut15, pong, []) 

这个进程执行tut15:pong()。Pong_PID是pong进程的进程标识符。接着创建一个名为“ping”的进程:

代码语言:javascript复制
 spawn(tut15, ping, [3, Pong_PID]), 

这个进程执行:

代码语言:javascript复制
 tut15:ping(3, Pong_PID) 

<0.36.0>是start函数的返回值。

“pong”进程现在这样:

代码语言:javascript复制
receive
    finished ->
        io:format("Pong finished~n", []);
    {ping, Ping_PID} ->
        io:format("Pong received ping~n", []),
        Ping_PID ! pong,
        pong()
end.

receive 结构用于使进程等待另一个进程的消息。它有下面的格式:

代码语言:javascript复制
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消息:

代码语言:javascript复制
 Ping_PID ! pong 

注意“!”运算符是如何发送消息的。“!”的语法是:

代码语言:javascript复制
 Pid ! Message 

即将消息(任何Erlang项)发送到Pid表示的进程。

在向“ping”进程发送了pong消息后,“pong”函数会调用自身,导致它重新回到receive结构等待另一条消息。

现在让我们看看“ping”进程。回忆一下它是这样开始的:

代码语言:javascript复制
 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进程发送一条消息:

代码语言:javascript复制
 Pong_PID ! {ping, self()}, 

self()返回执行self()的进程的pid,在这个是“ping”进程的pid。(回忆一下“pong”的代码,self()的值最终会到达之前所说的receive结构中的Ping_PID变量。)

现在"Ping"等待一个来自“pong”的答复:

代码语言:javascript复制
receive
    pong ->
        io:format("Ping received pong~n", [])
end,

当收到回复时它会输出"Ping received pong",在这之后ping函数也会调用自己。

代码语言:javascript复制
ping(N - 1, Pong_PID)

N-1使得第一个参数减一,直到它变成零。 当变成零时,ping/2的第一个clause就会被执行:

代码语言:javascript复制
ping(0, Pong_PID) ->
    Pong_PID !  finished,
    io:format("ping finished~n", []);

该函数会向pong进程发送原子finished(正如上面描述的这会使得pong结束进程),接着会输 "ping finished"。 然后"Ping"会因为没有事情做而终止。

3. 进程名注册

在之前的例子中,“pong”进程最先被创建,并将它的进程标识符给接下来创建的“ping”进程作为参数。也即是说,“ping”必须通过某种方式知道“pong”进程才能向它发送消息。有时独立启动的进程需要知道彼此的标识符。鉴于此Erlang提供了一种进程机制来给进程命名而不是在一堆函数中混乱传递PID参数,这种机制是通过内置函数register完成的。

代码语言:javascript复制
 register(some_atom, Pid) 

现在让我们使用下面的代码来重写ping pong 例子,给“pong”进程一个名字:

代码语言:javascript复制
-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]).
代码语言:javascript复制
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函数,

代码语言:javascript复制
 register(pong, spawn(tut16, pong, [])), 

同时做了启动“pong”线程,给线程命名两件事。在“ping”进程中,可以这样给“pong”进程发送消息:

代码语言:javascript复制
 pong ! {ping, self()}, 

ping/2 现在变成了ping/1,省去了Pong_PID参数(避免在各个函数中混乱传递Ping_PID/Pong_PID参数)

4. 分布式编程

让我们重写ping pong这个例子,使“ping”和“pong”在不同电脑上运行。第一件事是设置。Erlang的分布式实现提供了一个非常基础的验证机制来避免一台电脑不小心连接到Erlang分布式集群。Erlang集群的交流必须有一个相同的magic cookie。要实现这个最简单的方法是通过一个.erlang.cookie文件,将它放置于集群中的各台电脑(译注:即服务器,后文也译做“电脑(computer)”)的home目录,这样它们就能相互通信:

  • Windows系统上home目录可以由环境变量$HOME指定——你可能需要自行设置一下
  • Linux或UNIX可以忽略,只需要在你启动shell并执行cd(不附带任何参数)命令后所显示的目录下创建一个.erlang.cookie文件

.erlang.cookie文件包含了一行相同的原子。举个例子,在Linux或UNIX系统shell中

代码语言:javascript复制
$ cd
$ cat > .erlang.cookie
this_is_very_secret
$ chmod 400 .erlang.cookie

chmod命令将只允许文件的拥有者访问.erlang.cookie文件。这是需求不是必要。

当你启动一个Erlang系统,想和另一个Erlang系统通信,你必须给它一个名字,比如:

代码语言:javascript复制
$ erl -sname my_name

在后面我们会讨论更多关于这个的细节。如果你想实验一下分布式Erlang,但是你只有一台电脑,你可以在这台电脑上启动两个独立的Erlang系统,只需要给它们指定不同的名字。每个运行着Erlang系统的电脑叫做Erlang节点(Erlang node)

(注意: erl -sname假定所有节点都是用相同的IP,如果我们想在不同的IP上运行Erlang系统请使用 -name代替。但是IP地址必须给全。)

像下面一样修改ping pong例子使之运行在不同的节点:

代码语言:javascript复制
-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如下:

代码语言:javascript复制
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:

代码语言:javascript复制
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:

代码语言:javascript复制
(pong@gollum)1> tut17:start_pong(). true

在kosken节点上启动ping进程:

代码语言:javascript复制
(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”那边:

代码语言:javascript复制
(pong@gollum)2>
Pong received ping                 
Pong received ping                 
Pong received ping                 
Pong finished                      
(pong@gollum)2>

注意tut17的代码,你会注意到pong函数的代码没有改变,下面的代码也一样,它不关心ping进程所在的节点:

代码语言:javascript复制
{ping, Ping_PID} ->
    io:format("Pong received ping~n", []),
    Ping_PID ! pong,

因此,Erlang pid包含了进程在哪执行的信息。如果你知道一个进程的pid,就可以用“!”运算符发送消息,而不用考虑进程在不在相同的节点。 有一点不同是消息怎样发送给另一个节点上已注册的进程:

代码语言:javascript复制
{pong, Pong_Node} ! {ping, self()},

一个元组tuple {registered_name,node_name}用来代替 registered_name。

在钱的例子中,‘’ping”和“pong”由两个独立的Erlang节点的shell中启动,也就是说spawn可以在不同的节点上启动进程。

下面的例子又是ping pong程序,但是这一次“ping”在另一个节点启动:

代码语言:javascript复制
-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上这样做:

代码语言:javascript复制
(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系统会找到进程从哪启动,然后在那输出。

5. 一个完整的例子

现在写一个完整的例子,叫做“messenger”。messenger这个程序运行在不同的Erlang节点上登陆然后互相发送消息(message)。

在开始前,注意下面几点:

  • 这个例子只显示了消息传递的逻辑——并不打算提供一个友好的GUI,虽然这也可以用Erlang完成
  • 这类问题用OTP的一些设施更容易解决,因为它们能提供一些方法进行代码热更新等 (参见 OTP Design Principles).
  • 第一个程序有一些缺陷。后续版本会逐步修复它。The first program contains some inadequacies regarding handling of nodes which disappear. These are corrected in a later version of the program.

messenger允许创建客户端然后连接中央服务器,并服务器会知晓客户端是哪些、它们在哪。也就是说,用户不需要关系当前节点的名字和其他节点在哪就能发送消息。

messenger.erl文件如下:

代码语言:javascript复制
%%% 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]        �d 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.

要运行这个程序,你需要:

  • 配置server_node()函数
  • 把编译后的字节码 (messenger.beam) 复制到其它电脑,这样它们才能使用这些函数

接下来的例子是使用这个程序,在四个不同电脑上启动Erlang节点。如果你没有那么多电脑那么可以考虑在一台机器上启动不同的节点(译注:-sname,具体可以参见前面小结)。

四个Erlang节点分别是:messenger@super, c1@bilbo, c2@kosken, c3@gollum.

首先启动服务器节点messenger@supe:

代码语言:javascript复制
 (messenger@super)1> messenger:start_server(). true 

接着在c1@bilbo上登陆Peter:

代码语言:javascript复制
(c1@bilbo)1> messenger:logon(peter). true
logged_on

在c2@kosken上登陆James:

代码语言:javascript复制
(c2@kosken)1> messenger:logon(james). true
logged_on

Fred在c3@gollum上登陆:

代码语言:javascript复制
(c3@gollum)1> messenger:logon(fred). true
logged_on

现在Peter给Fred发送消息:

代码语言:javascript复制
(c1@bilbo)2> messenger:message(fred, "hello").
ok
sent

Fred收到消息并回复Peter一条消息然后注销:

代码语言:javascript复制
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发送消息:

代码语言:javascript复制
(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。

代码语言:javascript复制
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的工作方式类似,只是如果找到就删除它并返回剩余列表:

代码语言:javascript复制
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模块的大部分代码。让我们取一个片段看看它的细节。

第一个用户“发送”消息:

代码语言:javascript复制
 messenger:message(fred, "hello") 

在测试了客户端进程存在之后:

代码语言:javascript复制
 whereis(mess_client)  

将会发送一条消息给mess_client:

代码语言:javascript复制
 mess_client ! {message_to, fred, "hello"} 

它的实现是客户端向服务器发送消息:

代码语言:javascript复制
 {messenger, messenger@super} ! {self(), message_to, fred, "hello"}, 

然后等待服务器的回复。

把目光转向服务器,它收到消息然后调用:

代码语言:javascript复制
 server_transfer(From, fred, "hello", User_List), 

它检查User_List中的pid:

代码语言:javascript复制
 lists:keysearch(From, 1, User_List)  

如果keysearch返回原子false,引发错误,服务器会这样回复:

代码语言:javascript复制
 From ! {messenger, stop, you_are_not_logged_on} 

它将被客户端收到,然后客户端执行exit(normal)终止。如果keysearch返回{value,{From,Name}},很明显用户已经登录,他的名字(peter)会被绑定到Name上。

现在让我们调用:

代码语言:javascript复制
 server_transfer(From, peter, fred, "hello", User_List) 

注意server_transfer/5,它不同于server_transfer/4。另一个keysearch会在User_List上进行,然后返回fred客户端的pid:

代码语言:javascript复制
 lists:keysearch(fred, 2, User_List) 

这次Position指定为2,也就是tuple的第二个元素和fred进行匹配。如果返回原子false,fred就没有登录然后发送下面的消息:

代码语言:javascript复制
 From ! {messenger, receiver_not_found}; 

客户端会收到该条消息。

如果keysearch返回:

代码语言:javascript复制
 {value, {ToPid, fred}} 

会向fred发送:

代码语言:javascript复制
 ToPid ! {message_from, peter, "hello"},  

向peter发送:

代码语言:javascript复制
 From ! {messenger, sent}  

Fred'收到该条消息然后输出:

代码语言:javascript复制
{message_from, peter, "hello"} ->
    io:format("Message from ~p: ~p~n", [peter, "hello"])

Peter客户端在await_result函数调用中接收消息。

0 人点赞