3 并发编程
3.1 进程
使用 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 相同的方式以标准语法写入数据,但将打印表示形式超过一行的项分解成多行,并以合理的格式缩进每一行。它还尝试检测可打印字符的列表,并将它们输出为字符串”。
3.2 消息传递
在以下示例中,创建了两个进程,它们相互发送消息若干次。
-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”然后终止,因为它没有更多要做的工作。
3.3 注册进程名称
在上面的示例中,“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。
3.4 分布式编程
让我们重写 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 系统会找出进程是从哪里生成的,并将所有输出发送到那里。
3.5 一个更大的例子
现在举一个更大的例子,使用一个简单的 "信使"。信使是一个程序,允许用户在不同的节点上登录,并互相发送简单的消息。
在开始之前,请注意以下几点
-
本例只展示了消息传递逻辑,没有试图提供一个友好的图形用户界面,尽管这也可以在 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:keysearch 与 lists:keymember 类似,但它会返回 {value,Tuple_Found} 或原子 false。
lists 模块中有很多非常有用的函数。
一个 Erlang 进程(从概念上来说)会一直运行,直到它执行 receive,并且在消息队列中没有它想接收的消息。"从概念上来说" 是指 Erlang 系统在系统中的活动进程之间共享 CPU 时间。
进程终止时,没有更多的事情可以做,也就是说,它调用的最后一个函数只是返回,并没有调用另一个函数。进程终止的另一种方式是调用 exit/1。 exit/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 函数中收到消息。