Erlang 消息传递

下面的例子中创建了两个进程,它们相互之间会发送多个消息。

  1. -module(tut15).
  2. -export([start/0, ping/2, pong/0]).
  3. ping(0, Pong_PID) ->
  4. Pong_PID ! finished,
  5. io:format("ping finished~n", []);
  6. ping(N, Pong_PID) ->
  7. Pong_PID ! {ping, self()},
  8. receive
  9. pong ->
  10. io:format("Ping received pong~n", [])
  11. end,
  12. ping(N - 1, Pong_PID).
  13. pong() ->
  14. receive
  15. finished ->
  16. io:format("Pong finished~n", []);
  17. {ping, Ping_PID} ->
  18. io:format("Pong received ping~n", []),
  19. Ping_PID ! pong,
  20. pong()
  21. end.
  22. start() ->
  23. Pong_PID = spawn(tut15, pong, []),
  24. spawn(tut15, ping, [3, Pong_PID]).
  1. 1> c(tut15).
  2. {ok,tut15}
  3. 2> tut15: start().
  4. <0.36.0>
  5. Pong received ping
  6. Ping received pong
  7. Pong received ping
  8. Ping received pong
  9. Pong received ping
  10. Ping received pong
  11. ping finished
  12. Pong finished

start 函数先创建了一个进程,我们称之为 “pong”:

  1. Pong_PID = spawn(tut15, pong, [])

这个进程会执行 tut15:pong 函数。Pong_PID 是 “pong” 进程的进程标识符。接下来,start 函数又创建了另外一个进程 ”ping“:

  1. spawn(tut15,ping,[3,Pong_PID]),

这个进程执行:

  1. tut15:ping(3, Pong_PID)

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

”pong“ 进程完成下面的工作:

  1. receive
  2. finished ->
  3. io:format("Pong finished~n", []);
  4. {ping, Ping_PID} ->
  5. io:format("Pong received ping~n", []),
  6. Ping_PID ! pong,
  7. pong()
  8. end.

receive 关键字被进程用来接收从其它进程发送的的消息。它的使用语法如下:

  1. receive
  2. pattern1 ->
  3. actions1;
  4. pattern2 ->
  5. actions2;
  6. ....
  7. patternN
  8. actionsN
  9. end.

请注意,在 end 前的最后一个 actions 并没有 ";"。

Erlang 进程之间的消息可以是任何简单的 Erlang 项。比如说,可以是列表、元组、整数、原子、进程标识等等。

每个进程都有独立的消息接收队列。新接收的消息被放置在接收队列的尾部。当进程执行 receive 时,消息中第一个消息与与 receive 后的第一个模块进行匹配。如果匹配成功,则将该消息从消息队列中删除,并执行该模式后面的代码。

然而,如果第一个模式匹配失败,则测试第二个匹配。如果第二个匹配成功,则将该消息从消息队列中删除,并执行第二个匹配后的代码。如果第二个匹配也失败,则匹配第三个,依次类推,直到所有模式都匹配结束。如果所有匹配都失败,则将第一个消息留在消息队列中,使用第二个消息重复前面的过程。第二个消息匹配成功时,则执行匹配成功后的程序并将消息从消息队列中取出(将第一个消息与其余的消息继续留在消息队列中)。如果第二个消息也匹配失败,则尝试第三个消息,依次类推,直到尝试完消息队列所有的消息为止。如果所有消息都处理结束(匹配失败或者匹配成功被移除),则进程阻塞,等待新的消息的到来。上面的过程将会一直重复下去。

Erlang 实现是非常 “聪明” 的,它会尽量减少 receive 的每个消息与模式匹配测试的次数。

让我们回到 ping pong 示例程序。

“Pong” 一直等待接收消息。 如果收到原子值 finished,“Pong” 会输出 “Pong finished”,然后结束进程。如果收到如下形式的消息:

  1. {ping, Ping_PID}

则输出 “Pong received ping”,并向进程 “ping” 发送一个原子值消息 pong:

  1. Ping_PID ! pong

请注意这里是如何使用 “!” 操作符发送消息的。 “!” 操作符的语法如下所示:

  1. Pid ! Message

这表示将消息(任何 Erlang 数据)发送到进程标识符为 Pid 的进程的消息队列中。

将消息 pong 发送给进程 “ping” 后,“pong” 进程再次调用 pong 函数,这会使得再次回到 receive 等待下一个消息的到来。

下面,让我们一起去看看进程 “ping”,回忆一下它是从下面的地方开始执行的:

  1. tut15:ping(3, Pong_PID)

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

第二个子句将发送消息给 “pong” 进程:

  1. Pong_PID ! {ping, self()},

self() 函数返回当前进程(执行 self() 的进程)的进程标识符,在这儿为 “ping” 进程的进程标识符。(回想一下 “pong” 的代码,这个进程标识符值被存储在变量 Ping_PID 当中)

发送完消息后,“Ping” 接下来等待回复消息 “pong”:

  1. receive
  2. pong ->
  3. io:format("Ping received pong~n", [])
  4. end,

收到回复消息后,则输出 “Ping received pong”。之后 “ping” 也再次调用 ping 函数:

  1. ping(N - 1, Pong_PID)

N-1 使得第一个参数逐渐减小到 0。当其值变为 0 后,ping/2 函数的第一个子句会被执行。

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

此时,原子值 finished 被发送至 “pong” 进程(会导致进程结束),同时将“ping finished” 输出。随后,“Ping” 进程结束。