3  并发编程

3  并发编程

使用 Erlang 而不是其他函数式语言的主要原因之一是 Erlang 处理并发和分布式编程的能力。并发是指程序能够同时处理多个执行线程。例如,现代操作系统允许您同时使用文字处理器、电子表格、邮件客户端和打印作业。系统中的每个处理器 (CPU) 可能一次只处理一个线程(或作业),但它在作业之间切换的速度如此之快,以至于给人以同时运行所有作业的错觉。在 Erlang 程序中创建并行执行线程以及允许这些线程相互通信非常容易。在 Erlang 中,每个执行线程称为一个**进程**。

(旁注:术语“进程”通常用于执行线程彼此之间不共享任何数据时,而术语“线程”用于它们以某种方式共享数据时。Erlang 中的执行线程不共享任何数据,这就是为什么它们被称为进程的原因)。

Erlang BIF spawn 用于创建一个新进程:spawn(Module, Exported_Function, List of Arguments)。考虑以下模块

-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 启动两个 Erlang 进程,一个写入“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 返回一个**进程标识符**,或**pid**,它唯一标识进程。因此 <0.63.0> 是上面 spawn 函数调用的 pid。下一个示例展示了如何使用 pid。

还要注意,在 io:format 中使用了 ~p 而不是 ~w。引用手册:“~p 以与 ~w 相同的方式以标准语法写入数据,但将打印表示形式超过一行的项分解成多行,并以合理的格式缩进每一行。它还尝试检测可打印字符的列表,并将它们输出为字符串”。

在以下示例中,创建了两个进程,它们相互发送消息若干次。

-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”进程的进程标识。函数 start 现在创建另一个进程“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 项。也就是说,它们可以是列表、元组、整数、原子、pid 等等。

每个进程都有自己的输入队列,用于接收它收到的消息。接收到的新消息将放到队列的末尾。当进程执行 receive 时,队列中的第一条消息将与 receive 中的第一个模式进行匹配。如果匹配,消息将从队列中删除,并且将执行与模式相对应的操作。

但是,如果第一个模式不匹配,则测试第二个模式。如果匹配,消息将从队列中删除,并且将执行与第二个模式相对应的操作。如果第二个模式不匹配,则尝试第三个模式,依此类推,直到没有更多模式要测试为止。如果没有更多模式要测试,则第一个消息将保留在队列中,并尝试第二个消息。如果它与任何模式匹配,则执行适当的操作,并将第二个消息从队列中删除(保留第一个消息以及队列中的任何其他消息)。如果第二个消息不匹配,则尝试第三个消息,依此类推,直到到达队列的末尾。如果到达队列的末尾,进程将阻塞(停止执行)并等待直到接收到新消息,并重复此过程。

Erlang 实现是“巧妙的”,它最大限度地减少了每次消息与每个 receive 中的模式进行测试的次数。

现在回到 ping pong 示例。

“Pong”正在等待消息。如果收到原子 finished,“pong”将“Pong finished”写入输出,并且由于它没有更多要做的工作,因此会终止。如果它收到一个格式为

{ping, Ping_PID}

的消息,它将“Pong received ping”写入输出,并将原子 pong 发送给进程“ping”

Ping_PID ! pong

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

Pid ! Message

也就是说,将 Message(任何 Erlang 项)发送给具有标识 Pid 的进程。

向进程“ping”发送消息 pong 后,“pong”再次调用 pong 函数,这会导致它再次返回到 receive 并等待另一条消息。

现在让我们看看进程“ping”。回想一下,它是通过执行以下操作启动的

tut15:ping(3, Pong_PID)

查看函数 ping/2,由于第一个参数的值为 3(不为 0),因此执行 ping/2 的第二个子句(第一个子句头部是 ping(0,Pong_PID),第二个子句头部是 ping(N,Pong_PID),所以 N 变为 3)。

第二个子句向“pong”发送一条消息

Pong_PID ! {ping, self()},

self() 返回执行 self() 的进程的 pid,在本例中是“ping”的 pid。(回想一下“pong”的代码,这最终出现在之前解释过的 receive 中的变量 Ping_PID 中)。

“Ping”现在等待来自“pong”的回复

receive
    pong ->
        io:format("Ping received pong~n", [])
end,

当收到此回复时,它会写入“Ping received pong”,之后“ping”再次调用 ping 函数。

ping(N - 1, Pong_PID)

N-1 使第一个参数递减,直到它变为 0。当这种情况发生时,将执行 ping/2 的第一个子句

ping(0, Pong_PID) ->
    Pong_PID !  finished,
    io:format("ping finished~n", []);

原子 finished 被发送给“pong”(导致它如上所述终止),并且“ping finished”被写入输出。“Ping”然后终止,因为它没有更多要做的工作。

在上面的示例中,“pong”首先被创建,以便能够在启动“ping”时提供“pong”的标识。也就是说,“ping”必须以某种方式知道“pong”的标识,才能向它发送消息。有时,需要知道彼此标识的进程是独立启动的。因此,Erlang 提供了一种机制,使进程可以被赋予名称,以便这些名称可以作为标识符而不是 pid 使用。这可以通过使用 register BIF 来实现

register(some_atom, Pid)

现在让我们使用此方法重写 ping pong 示例,并将名称 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”进程并为它赋予名称 pong。在“ping”进程中,可以通过以下方式向 pong 发送消息

pong ! {ping, self()},

ping/2 现在变为 ping/1,因为不再需要参数 Pong_PID

让我们重写 ping pong 程序,使“ping”和“pong”在不同的计算机上运行。首先,需要做几件事才能使其正常工作。分布式 Erlang 实现提供了一种非常基本的身份验证机制,以防止无意中访问另一台计算机上的 Erlang 系统。相互通信的 Erlang 系统必须具有相同的**魔法 Cookie**。实现这一点的最简单方法是在您将要运行相互通信的 Erlang 系统的所有计算机上的主目录中创建一个名为 .erlang.cookie 的文件

  • 在 Windows 系统上,主目录是环境变量 $HOME 指向的目录 - 您可能需要设置它。
  • 在 Linux 或 UNIX 上,您可以安全地忽略这一点,只需在执行命令 cd 而不带任何参数后到达的目录中创建一个名为 .erlang.cookie 的文件即可。

.erlang.cookie 文件应包含一行具有相同原子的内容。例如,在 Linux 或 UNIX 上,在 OS 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 节点**。

(注意:erl -sname 假设所有节点都在同一个 IP 域中,我们只能使用 IP 地址的第一部分,如果我们要在不同的域中使用节点,我们将使用 -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 上(在 Linux/UNIX 系统上)

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" 进程启动了(从上面的代码可以看出,start_ping 函数的一个参数是 "pong" 运行的 Erlang 系统的节点名称)

(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()},

使用元组 {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()]).

假设一个名为 ping 的 Erlang 系统(但不是 "ping" 进程)已经在 kosken 上启动,那么在 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 系统会找出进程是从哪里生成的,并将所有输出发送到那里。

现在举一个更大的例子,使用一个简单的 "信使"。信使是一个程序,允许用户在不同的节点上登录,并互相发送简单的消息。

在开始之前,请注意以下几点

  • 本例只展示了消息传递逻辑,没有试图提供一个友好的图形用户界面,尽管这也可以在 Erlang 中实现。

  • 这类问题可以通过使用 OTP 中的功能更容易地解决,OTP 也提供了一些方法来动态更新代码等等(参见 OTP 设计原则)。

  • 第一个程序在处理消失的节点方面存在一些不足之处。这些问题将在程序的后续版本中得到纠正。

信使的设置方法是允许 "客户机" 连接到中央服务器,并说明他们是谁以及他们在哪里。也就是说,用户不需要知道另一个用户所在的 Erlang 节点的名称来发送消息。

文件 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@super.

%%% 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.

要使用这个程序,你需要

  • 配置 server_node() 函数。
  • 将编译后的代码 (messenger.beam) 复制到每台计算机上启动 Erlang 的目录。

在以下使用这个程序的例子中,节点是在四台不同的计算机上启动的。如果你在网络上没有那么多机器可用,你可以在同一台机器上启动多个节点。

启动了四个 Erlang 节点:messenger@super、c1@bilbo、c2@kosken、c3@gollum。

首先,启动 messenger@super 上的服务器

(messenger@super)1> messenger:start_server().
true

现在 Peter 在 c1@bilbo 上登录

(c1@bilbo)1> messenger:logon(peter).
true
logged_on

James 在 c2@kosken 上登录

(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) 会遍历元组列表,查看每个元组中 Position 位置的值是否与 Key 相同。第一个元素是位置 1。如果在 Position 位置的值与 Key 相同的元组中找到了,则返回 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:keysearchlists:keymember 类似,但它会返回 {value,Tuple_Found} 或原子 false

lists 模块中有很多非常有用的函数。

一个 Erlang 进程(从概念上来说)会一直运行,直到它执行 receive,并且在消息队列中没有它想接收的消息。"从概念上来说" 是指 Erlang 系统在系统中的活动进程之间共享 CPU 时间。

进程终止时,没有更多的事情可以做,也就是说,它调用的最后一个函数只是返回,并没有调用另一个函数。进程终止的另一种方式是调用 exit/1exit/1 的参数具有特殊含义,这将在后面讨论。在本例中,执行了 exit(normal),其效果与进程运行完所有要调用的函数相同。

BIF whereis(RegisteredName) 检查是否存在名为 RegisteredName 的注册进程。如果存在,则返回该进程的 pid。如果不存在,则返回原子 undefined

你现在应该能够理解信使模块中的大部分代码了。让我们详细研究一个案例:从一个用户向另一个用户发送消息。

第一个用户在上面的例子中通过以下方式 "发送" 消息

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),

这会检查 pid From 是否在 User_List

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 不同。在 User_List 上执行另一个 keysearch 操作,以查找与 fred 对应的客户机的 pid

lists:keysearch(fred, 2, User_List)

这次使用参数 2,也就是元组中的第二个元素。如果这返回原子 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 函数中收到消息。