查看源代码 概述

OTP 设计原则 定义了如何根据进程、模块和目录来构建 Erlang 代码。

监督树

Erlang/OTP 中的一个基本概念是监督树。这是一种基于工作者监督者理念的进程结构模型。

  • 工作者是执行计算和其他实际工作的进程。
  • 监督者是监控工作者的进程。如果出现问题,监督者可以重启工作者。
  • 监督树是将代码分层安排为监督者和工作者的结构,这使得设计和编程容错软件成为可能。

在下图中,方框代表监督者,圆圈代表工作者

---
title: Supervision Tree
---
flowchart
    sup1[Type 1 Supervisor] --- sup2[Type 1 Supervisor] --- worker1((worker))
    sup1 --- sup1a[Type A Supervisor]

    sup1a --- sup2a[Type A Supervisor] --- worker2((worker))
    sup1a --- sup3[Type 1 Supervisor]

    sup3 --- worker3((worker))
    sup3 --- worker4((worker))

行为

在监督树中,许多进程具有相似的结构并遵循相似的模式。例如,监督者共享相似的结构,唯一的区别在于他们监督的子进程。许多工作者是服务器-客户端关系中的服务器、有限状态机或事件处理程序。

行为是对这些常见模式的正式化。其思想是将进程的代码划分为通用部分(行为模块)和特定部分(回调模块)。

行为模块是 Erlang/OTP 的一部分。要实现诸如监督者之类的进程,用户只需实现回调模块,该模块需要导出一组预定义的函数,即回调函数

以下示例说明了如何将代码划分为通用部分和特定部分。考虑以下(用纯 Erlang 编写的)代码,用于一个简单的服务器,该服务器跟踪多个“通道”。其他进程可以通过调用函数 alloc/0free/1 分别分配和释放通道。

-module(ch1).
-export([start/0]).
-export([alloc/0, free/1]).
-export([init/0]).

start() ->
    spawn(ch1, init, []).

alloc() ->
    ch1 ! {self(), alloc},
    receive
        {ch1, Res} ->
            Res
    end.

free(Ch) ->
    ch1 ! {free, Ch},
    ok.

init() ->
    register(ch1, self()),
    Chs = channels(),
    loop(Chs).

loop(Chs) ->
    receive
        {From, alloc} ->
            {Ch, Chs2} = alloc(Chs),
            From ! {ch1, Ch},
            loop(Chs2);
        {free, Ch} ->
            Chs2 = free(Ch, Chs),
            loop(Chs2)
    end.

服务器的代码可以重写为通用部分 server.erl

-module(server).
-export([start/1]).
-export([call/2, cast/2]).
-export([init/1]).

start(Mod) ->
    spawn(server, init, [Mod]).

call(Name, Req) ->
    Name ! {call, self(), Req},
    receive
        {Name, Res} ->
            Res
    end.

cast(Name, Req) ->
    Name ! {cast, Req},
    ok.

init(Mod) ->
    register(Mod, self()),
    State = Mod:init(),
    loop(Mod, State).

loop(Mod, State) ->
    receive
        {call, From, Req} ->
            {Res, State2} = Mod:handle_call(Req, State),
            From ! {Mod, Res},
            loop(Mod, State2);
        {cast, Req} ->
            State2 = Mod:handle_cast(Req, State),
            loop(Mod, State2)
    end.

和一个回调模块 ch2.erl

-module(ch2).
-export([start/0]).
-export([alloc/0, free/1]).
-export([init/0, handle_call/2, handle_cast/2]).

start() ->
    server:start(ch2).

alloc() ->
    server:call(ch2, alloc).

free(Ch) ->
    server:cast(ch2, {free, Ch}).

init() ->
    channels().

handle_call(alloc, Chs) ->
    alloc(Chs). % => {Ch,Chs2}

handle_cast({free, Ch}, Chs) ->
    free(Ch, Chs). % => Chs2

请注意以下几点

  • server 中的代码可以重用来构建许多不同的服务器。
  • 服务器名称,在本例中为原子 ch2,对客户端函数的用户是隐藏的。这意味着可以更改名称而不影响他们。
  • 协议(发送到服务器和从服务器接收的消息)也是隐藏的。这是一个良好的编程实践,允许更改协议而无需更改使用接口函数的代码。
  • server 的功能可以扩展,而无需更改 ch2 或任何其他回调模块。

在上面的 ch1.erlch2.erl 中,channels/0alloc/1free/2 的实现被故意省略了,因为它与示例无关。为了完整起见,下面给出了一种编写这些函数的方法。这只是一个示例,实际的实现必须能够处理诸如耗尽要分配的通道等情况。

channels() ->
   {_Allocated = [], _Free = lists:seq(1, 100)}.

alloc({Allocated, [H|T] = _Free}) ->
   {H, {[H|Allocated], T}}.

free(Ch, {Alloc, Free} = Channels) ->
   case lists:member(Ch, Alloc) of
      true ->
         {lists:delete(Ch, Alloc), [Ch|Free]};
      false ->
         Channels
   end.

不使用行为编写的代码可能更有效率,但效率的提高是以通用性为代价的。以一致的方式管理系统中所有应用程序的能力很重要。

使用行为也使得更容易阅读和理解其他程序员编写的代码。即兴的编程结构,虽然可能更有效率,但总是更难理解。

server 模块对应于 Erlang/OTP 行为 gen_server,为了简化起见,进行了大幅简化。

标准的 Erlang/OTP 行为是

编译器理解模块属性 -behaviour(Behaviour),并发出关于缺少回调函数的警告,例如

-module(chs3).
-behaviour(gen_server).
...

3> c(chs3).
./chs3.erl:10: Warning: undefined call-back function handle_call/3
{ok,chs3}

应用程序

Erlang/OTP 附带了许多组件,每个组件都实现了一些特定的功能。在 Erlang/OTP 术语中,组件被称为应用程序。Erlang/OTP 应用程序的示例包括 Mnesia,它具有编程数据库服务所需的一切,以及 Debugger,用于调试 Erlang 程序。基于 Erlang/OTP 的最小系统由以下两个应用程序组成

  • Kernel - 运行 Erlang 所必需的功能
  • STDLIB - Erlang 标准库

应用程序概念同时适用于程序结构(进程)和目录结构(模块)。

最简单的应用程序没有任何进程,而是由功能模块的集合组成。这样的应用程序称为库应用程序。库应用程序的一个示例是 STDLIB。

具有进程的应用程序最容易使用标准行为作为监督树来实现。

如何在 应用程序 中描述如何编写应用程序。

发布

发布是由 Erlang/OTP 应用程序的子集和一组用户特定的应用程序组成的完整系统。

如何在 发布 中描述如何编写发布。

如何在目标环境中安装发布在系统原则中的创建和升级目标系统 中描述。

发布处理

发布处理是在(可能)运行的系统中,在不同版本的发布之间进行升级和降级。如何在 发布处理 中描述如何进行此操作。