Erlang Concurrent 併發進階

寫在前面的話


1. 進程

使用Erlang而不是其他函數式編程語言的主要原因之一就是Erlang的併發處理能力和分佈式編程。併發意味着程序可以在同一時刻執行多個線程。舉個例子,操作系統允許你在同一時刻運行文字處理程序,電子表格程序,郵件客戶端,和打印任務。系統中的每個處理器(CPU)有可能只處理一個線程,但是它以一定頻率交換這些線程,給我們造成一種多個程序是在同一時刻執行的假象。在一個Erlang程序中很容易創建並行執行(parallel execution)的線程,並且運行這些這些線程互相通信。Erlang中,每個執行線程稱之爲進程(process)

(旁白:術語“進程(process)”通常用於各個執行線程不共享數據,術語‘’線程(thread)”用於當它們以某種方式共享數據。Erlang執行線程不共享數據,這就是爲什麼它們叫做進程的原因)

Erlang內置函數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啓動兩個進程,一個輸出“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。

同時還要注意在io:format中用~p代替~w。引用手冊的話:“~p和~w以相同的方式輸出標準語,但是如果輸出表示的項比一行長會合理的折斷成多行。它也嘗試去檢測一個可輸出的字符列表並將至以字符串的形式輸出。”

(譯註:這裏舉個例子(數據來源於官方),在shell中輸入:

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. 消息傳遞

在接下來的例子中創建了兩個進程,它們互相發送一些消息。

-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"會因爲沒有事情做而終止。

3. 進程名註冊

在之前的例子中,“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參數)

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中

$ 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系統會找到進程從哪啓動,然後在那輸出。


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文件如下:

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

要運行這個程序,你需要:

  • 配置server_node()函數
  • 把編譯後的字節碼 (messenger.beam) 複製到其它電腦,這樣它們才能使用這些函數

接下來的例子是使用這個程序,在四個不同電腦上啓動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函數調用中接收消息。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章