极客学院团队出品 · 更新于 2018-11-28 11:00:43

第 5 章 进程、服务及消息

5.1 在一个模块中实现进程

实现单一进程的代码应该包含在一个模块中。进程可以调用任何库例程中的函数,但是进程的“顶层循环”代码应该包含在单独的一个模块中。进程顶层循环的代码不能分散在几个模块中——这样做会使控制流程复杂化,变得极难理解。这并不意味着不应该使用通用服务库,这些库有助于构建控制流。

相反,应该用单独的一个模块实现一种(不能再多了)进程。含有不同进程代码的模块会变得非常难于理解。每个独立进程的代码都应位于各自独立的一个模块中。

5.2 使用进程来构建系统

进程是基本的系统构建元素。但当可以使用函数调用时,就不要再使用进程和消息传递机制了。

5.3 注册进程

注册进程的注册名必须和模块名保持相同,从而易于查找进程代码。

只有注册进程才应该留存较长时间。

5.4 将一个并行进程赋予系统中的每个真正的并发行为

在确定是否使用顺序进程或并行进程来实现时,考虑问题的本质结构无疑能使结论变得清晰。主要原则如下:

“使用一个并行进程来对真实案例中的每个真正并发行为进行建模。”

如果在实际案例中,并行处理器与真正并行的行为之间能够建立起一对一的数量映射关系,程序就将变得易于理解。

5.5 Each process should only have one "role" 每个进程都应该只担当一个“角色”

进程扮演着系统的不同角色,下面以客户端-服务器模型为例。

一个进程应该尽量只担当一个角色,比如,它可以是服务器,也可以是客户端,但不能将两者混合起来。

Other roles which process might have are:

进程可能具有的其他角色包括:

Supervisor(监督者):查看其他进程,如果它们失败,则负责重启这些进程。
Worker(工作者):一种常见的工作进程(有可能会出现错误)。
Trusted Worker(可信工作者):不允许出现错误。

5.6 对于服务器和协议处理器,无论在什么情况下,都要尽量使用通用函数

在很多情况下,使用通用服务器程序都是一种非常好的方案,比如用标准库实现的 generic 服务器。使用通用服务器会极大简化整体的系统结构。

这一点也适用于系统中绝大多数协议处理软件。

5.7 Tag messages

所有的消息都应该加上标记。这样能使接收语句的顺序变得不那么重要,新消息的实现也容易了很多。

不要这样编程:

loop(State) ->
  receive
    ...
    {Mod, Funcs, Args} -> % Don't do this
      apply(Mod, Funcs, Args},
      loop(State);
    ...
  end.

新消息 {get_status_info, From, Option} 如果被放在 {Mod, Func, Args} 消息后面,就会引发冲突。

如果消息同步,返回的消息将用一个新的原子进行标记,目的是为了标记这是返回消息。例如:假如传入消息的标记为 get_status_info,则返回消息标记为status_info。另外,方便调试也是选择不同标记的一个理由。

下面这个方法就很不错:

loop(State) ->
  receive
    ...
    {execute, Mod, Funcs, Args} -> % Use a tagged message.
      apply(Mod, Funcs, Args},
      loop(State);
    {get_status_info, From, Option} ->
      From ! {status_info, get_status_info(Option, State)},
      loop(State);    
    ...
  end.

5.8 清空未知消息

每个服务器都应该在至少一个 receive 语句中保存一个 Other 的替代方案,这能避免消息队列堵塞。范例如下:


main_loop() ->
  receive
    {msg1, Msg1} -> 
      ...,
      main_loop();
    {msg2, Msg2} ->
      ...,
      main_loop();
    Other -> % 清空消息队列
      error_logger:error_msg(
          "Error: Process ~w got unknown msg ~w~n.", 
          [self(), Other]),
      main_loop()
  end.  

5.9 编写尾部递归的服务器

所有的服务器必须实现尾部递归,否则服务器就将不断消耗系统内存,直至用光它们。

不要像这样编程:


loop() ->
  receive
    {msg1, Msg1} -> 
      ...,
      loop();
    stop ->
      true;
    Other ->
      error_logger:log({error, {process_got_other, self(), Other}}),
      loop()
  end,
  io:format("Server going down").                % 不要这么做  
                % This is NOT tail-recursive

下面的方法才是正确的:


loop() ->
  receive
    {msg1, Msg1} -> 
      ...,
      loop();
    stop ->
      io:format("Server going down");
    Other ->
      error_logger:log({error, {process_got_other, self(), Other}}),
      loop()
  end. % This is tail-recursive  

如果使用一些服务器库,比如说generic,就不会犯下这种错误。

5.10 接口函数

尽量在接口中利用函数,而不要直接发送消息,并且要封装传入接口函数的消息。这其中有几种例外情况。

消息协议是内部消息,对其他模块来说,它应该是不透明的。

下面是接口函数的一个范例:

-module(fileserver).
-export([start/0, stop/0, open_file/1, ...]).

open_file(FileName) ->
  fileserver ! {open_file_request, FileName},
  receive
    {open_file_response, Result} -> Result
  end.

...<code>...  

5.11 超时

Be careful when using after in receive statements. Make sure that you handle the case when the message arrives later (See "Flush unknown messages" on page 16.).

receive 语句中使用 after 时要格外小心。一定要确保当消息到达时再处理案例(参看 5.8 节内容)。

5.12 捕获退出

尽量减少捕获退出信号的进程数目。进程要么捕获退出,要么就根本不捕获。在实际编码时,让进程对是否捕获退出进行“切换”,是非常糟糕的一种实践。