檢視原始碼 並行程式設計

程序

使用 Erlang 而不是其他函數式語言的主要原因之一,是 Erlang 處理並行和分散式程式設計的能力。並行指的是能夠同時處理多個執行緒的程式。例如,現代作業系統允許您同時執行文字處理器、試算表、郵件客戶端和列印作業。系統中的每個處理器 (CPU) 可能一次只處理一個執行緒(或作業),但它會在作業之間快速切換,讓您感覺它們都在同時執行。在 Erlang 程式中建立並行執行緒並允許這些執行緒相互通訊很容易。在 Erlang 中,每個執行緒都稱為程序

(題外話:術語「程序」通常用於執行緒之間不共享任何資料的情況,而術語「執行緒」用於它們以某種方式共享資料的情況。Erlang 中的執行緒不共享資料,這就是它們被稱為程序的原因)。

Erlang BIF spawn 用於建立新的程序:spawn(模組, 導出的函式, 參數列表)。請考慮以下模組

-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/2 中使用的是 ~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 的程序。

在將訊息 pong 傳送至程序「ping」之後,「pong」會再次呼叫 pong 函式,這會使其回到 receive,並等待另一個訊息。

現在讓我們看看程序「ping」。回想一下,它是透過執行

tut15:ping(3, Pong_PID)

來啟動的。查看函式 ping/2,會執行 ping/2 的第二個子句,因為第一個引數的值為 3 (非 0) (第一個子句標頭為 ping(0,Pong_PID),第二個子句標頭為 ping(N,Pong_PID),因此 N 變成 3)。

第二個子句會將訊息傳送至「pong」

Pong_PID ! {ping, self()},

self/0 會傳回執行 self/0 的程序的 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 檔案應該包含一行與節點名稱相同的原子(atom)。例如,在 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 節點*。

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

使用一個 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 系統(但不是 "ping" 處理程序),那麼在 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)。訊息傳遞器是一個程式,允許使用者在不同的節點上登入,並互相發送簡單的訊息。

在開始之前,請注意以下事項:

  • 此範例僅顯示訊息傳遞邏輯 - 沒有嘗試提供良好的圖形使用者介面,儘管這也可以在 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) 會檢查 tuple 的 list,並查看每個 tuple 中的 Position 是否與 Key 相同。第一個元素的位置是 1。如果找到一個 tuple,其中 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 的工作方式相同,但會刪除找到的第一個 tuple(如果有的話),並返回剩餘的 list。

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} 或 atom false

lists 模組中有很多非常有用的函數。

一個 Erlang 處理程序(概念上)會持續運行,直到它執行 receive,並且訊息佇列中沒有它想要接收的訊息。這裡使用「概念上」是因為 Erlang 系統會在系統中的活動處理程序之間共享 CPU 時間。

當處理程序沒有其他事情要做時,它就會終止。也就是說,它呼叫的最後一個函數只是返回,而不會呼叫另一個函數。處理程序終止的另一種方式是呼叫exit/1exit/1 的參數具有特殊的意義,稍後會討論。在此範例中,執行 exit(normal),其效果與處理程序用完要呼叫的函數相同。

BIF whereis(RegisteredName) 會檢查是否存在名為 RegisteredName 的已註冊處理程序。如果存在,則會返回該處理程序的 pid。如果不存在,則返回 atom 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 返回 atom 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)

這次使用參數 2,這是 tuple 中的第二個元素。如果這返回 atom 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 函數中接收到訊息。