查看源代码 概述
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/0
和 free/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.erl
和 ch2.erl
中,channels/0
、alloc/1
和 free/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 应用程序的子集和一组用户特定的应用程序组成的完整系统。
如何在 发布 中描述如何编写发布。
如何在目标环境中安装发布在系统原则中的创建和升级目标系统 中描述。
发布处理
发布处理是在(可能)运行的系统中,在不同版本的发布之间进行升级和降级。如何在 发布处理 中描述如何进行此操作。