强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Erlang/OTP 完全指南 / 10 - OTP 基础

第 10 章:OTP 基础 — GenServer、Supervisor、Application

OTP 是 Erlang 构建可靠系统的核心框架。本章学习 OTP 三大支柱:GenServer(通用服务器)、Supervisor(监督者)和 Application(应用)。


10.1 OTP 设计原则

10.1.1 为什么需要 OTP?

手写进程循环的问题:

%% 手写的服务器循环(有缺陷)
loop(State) ->
    receive
        {get, Key} ->
            %% 没有回复!
            loop(State);
        {set, Key, Value} ->
            NewState = maps:put(Key, Value, State),
            loop(NewState)
        %% 没有处理退出、超时、代码升级等
    end.

OTP 封装了这些模式:

手写循环的问题OTP 解决方案
没有统一的请求/响应模式GenServer 回调
没有优雅关闭terminate/2 回调
没有状态初始化init/1 回调
没有代码热升级code_change/3
没有错误处理Supervisor 重启
没有生命周期管理Application 行为

10.2 GenServer

10.2.1 什么是 GenServer?

GenServer(Generic Server)封装了有状态的服务进程,提供标准的回调接口。

%% counter.erl - 简单计数器
-module(counter).
-behaviour(gen_server).

%% API
-export([start_link/0, increment/1, decrement/1, get_value/0, stop/0]).

%% gen_server 回调
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3]).

%% ===== API =====

start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, 0, []).

increment(N) ->
    gen_server:call(?MODULE, {increment, N}).

decrement(N) ->
    gen_server:call(?MODULE, {decrement, N}).

get_value() ->
    gen_server:call(?MODULE, get_value).

stop() ->
    gen_server:stop(?MODULE).

%% ===== Callbacks =====

init(InitialCount) ->
    io:format("Counter started with ~p~n", [InitialCount]),
    {ok, InitialCount}.

handle_call({increment, N}, _From, State) ->
    NewState = State + N,
    {reply, NewState, NewState};
handle_call({decrement, N}, _From, State) ->
    NewState = State - N,
    {reply, NewState, NewState};
handle_call(get_value, _From, State) ->
    {reply, State, State}.

handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    io:format("Counter stopped~n"),
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

10.2.2 GenServer 回调详解

init/1

%% 初始化函数,在进程启动时调用
init(Args) ->
    %% 成功返回
    {ok, State}              %% 正常启动
    {ok, State, Timeout}     %% 带超时
    {ok, State, hibernate}   %% 休眠(节省内存)

    %% 失败返回
    {stop, Reason}           %% 启动失败
    ignore                   %% 忽略启动

handle_call/3 — 同步调用

%% gen_server:call(Server, Request) 调用此回调
handle_call(Request, From, State) ->
    %% Request: 请求消息
    %% From: {Pid, Tag},用于回复
    %% State: 当前状态

    %% 返回格式
    {reply, Reply, NewState}           %% 回复并继续
    {reply, Reply, NewState, Timeout}  %% 带超时
    {noreply, NewState}                %% 不回复(稍后手动 gen_server:reply)
    {noreply, NewState, hibernate}     %% 不回复并休眠
    {stop, Reason, Reply, NewState}    %% 回复并停止
    {stop, Reason, NewState}           %% 不回复并停止

handle_cast/2 — 异步消息

%% gen_server:cast(Server, Request) 调用此回调
handle_cast(Msg, State) ->
    %% 返回格式
    {noreply, NewState}              %% 继续
    {noreply, NewState, Timeout}     %% 带超时
    {stop, Reason, NewState}         %% 停止

handle_info/2 — 其他消息

%% 直接发送给进程的消息(非 call/cast)调用此回调
%% 例如:链接进程的退出消息、定时器消息等
handle_info(Info, State) ->
    %% 返回格式同 handle_cast
    {noreply, NewState}.

terminate/2 — 清理函数

%% 进程退出时调用(需要 trap_exit)
terminate(Reason, State) ->
    %% Reason: normal | shutdown | {shutdown, Term} | Term
    ok.

10.2.3 call vs cast vs info

方式函数同步/异步阻塞使用场景
callgen_server:call/2,3同步需要返回值
castgen_server:cast/2异步不需要返回值
infoPid ! Msg异步定时器、系统消息

10.2.4 带命名的 GenServer

%% 全局注册
gen_server:start_link({local, my_server}, ?MODULE, Args, Opts).
%% 本地访问:gen_server:call(my_server, Req)

%% 全局注册(跨节点)
gen_server:start_link({global, my_server}, ?MODULE, Args, Opts).
%% 全局访问:gen_server:call({global, my_server}, Req)

%% 不注册
gen_server:start_link(?MODULE, Args, Opts).
%% 必须用 PID 访问

10.3 Supervisor

10.3.1 基本概念

Supervisor 监控子进程,当子进程崩溃时自动重启:

%% my_sup.erl
-module(my_sup).
-behaviour(supervisor).

-export([start_link/0, init/1]).

start_link() ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).

init([]) ->
    Children = [
        #{
            id => counter,
            start => {counter, start_link, []},
            restart => permanent,
            shutdown => 5000,
            type => worker,
            modules => [counter]
        }
    ],
    {ok, {#{strategy => one_for_one, intensity => 5, period => 10}, Children}}.

10.3.2 子进程规范

字段说明
id子进程唯一标识atom
start启动函数{M, F, A}
restart重启策略permanent | transient | temporary
shutdown关闭方式brutal_kill | integer() | infinity
type进程类型worker | supervisor
modules模块列表[Module]
restart 策略说明
permanent总是重启
transient只在非正常退出时重启
temporary从不重启
shutdown 方式说明
brutal_kill立即杀死
integer()等待毫秒后杀死
infinity无限等待

10.3.3 重启策略

策略说明
one_for_one只重启崩溃的子进程
one_for_all一个崩溃,全部重启
rest_for_one崩溃的和之后启动的都重启
simple_one_for_one动态子进程(同类型)
one_for_one:
  A B C  →  A ✗ C  →  A' B C (只重启 A)

one_for_all:
  A B C  →  A ✗ C  →  A' B' C' (全部重启)

rest_for_one:
  A B C  →  A ✗ C  →  A B' C' (重启 B 和 C)

10.3.4 强度与周期

#{intensity => 5, period => 10}
%% 在 10 秒内最多重启 5 次
%% 如果超过这个频率,Supervisor 自身也会终止
%% 这是为了防止无限重启循环

10.4 Application

10.4.1 基本结构

%% my_app_app.erl - Application 回调模块
-module(my_app_app).
-behaviour(application).

-export([start/2, stop/1]).

start(_StartType, _StartArgs) ->
    my_app_sup:start_link().

stop(_State) ->
    ok.

10.4.2 应用描述文件

%% src/my_app.app.src
{application, my_app, [
    {description, "My Application"},
    {vsn, "0.1.0"},
    {registered, [counter, my_sup]},
    {mod, {my_app_app, []}},
    {applications, [kernel, stdlib]},
    {env, [
        {port, 8080},
        {max_connections, 1000}
    ]},
    {modules, []},
    {licenses, ["Apache-2.0"]},
    {links, []}
]}.

10.4.3 应用配置

%% config/sys.config
[
    {my_app, [
        {port, 8080},
        {max_connections, 1000},
        {log_level, info}
    ]}
].
%% 读取配置
application:get_env(my_app, port).          %% {ok, 8080}
application:get_env(my_app, undefined_key). %% undefined
application:get_env(my_app, port, 9090).    %% 8080(带默认值)

10.5 实战:键值存储服务

%% kv_store.erl
-module(kv_store).
-behaviour(gen_server).

%% API
-export([start_link/0, put/2, get/1, get/2, delete/1, list_all/0, stop/0]).

%% Callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3]).

%% ===== API =====

start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

put(Key, Value) ->
    gen_server:call(?MODULE, {put, Key, Value}).

get(Key) ->
    gen_server:call(?MODULE, {get, Key}).

get(Key, Default) ->
    case gen_server:call(?MODULE, {get, Key}) of
        {ok, Value} -> Value;
        not_found -> Default
    end.

delete(Key) ->
    gen_server:cast(?MODULE, {delete, Key}).

list_all() ->
    gen_server:call(?MODULE, list_all).

stop() ->
    gen_server:stop(?MODULE).

%% ===== Callbacks =====

init([]) ->
    {ok, #{}}.

handle_call({put, Key, Value}, _From, State) ->
    NewState = State#{Key => Value},
    {reply, ok, NewState};
handle_call({get, Key}, _From, State) ->
    case maps:find(Key, State) of
        {ok, Value} -> {reply, {ok, Value}, State};
        error -> {reply, not_found, State}
    end;
handle_call(list_all, _From, State) ->
    {reply, maps:to_list(State), State}.

handle_cast({delete, Key}, State) ->
    NewState = maps:remove(Key, State),
    {noreply, NewState}.

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.
$ rebar3 shell
1> kv_store:start_link().
{ok, <0.123.0>}
2> kv_store:put(name, "Alice").
ok
3> kv_store:put(age, 25).
ok
4> kv_store:get(name).
{ok, "Alice"}
5> kv_store:get(height, 0).
0
6> kv_store:list_all().
[{age,25},{name,"Alice"}]

10.6 监督树示例

%% my_sup.erl
-module(my_sup).
-behaviour(supervisor).
-export([start_link/0, init/1]).

start_link() ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).

init([]) ->
    Children = [
        #{
            id => kv_store,
            start => {kv_store, start_link, []},
            restart => permanent,
            shutdown => 5000,
            type => worker
        },
        #{
            id => event_logger,
            start => {event_logger, start_link, []},
            restart => permanent,
            shutdown => 5000,
            type => worker
        }
    ],
    SupFlags = #{
        strategy => one_for_one,
        intensity => 5,
        period => 10
    },
    {ok, {SupFlags, Children}}.

10.7 注意事项

⚠️ 常见错误

错误原因解决
{error, {already_started, Pid}}重复启动命名进程检查是否已启动
{error, noproc}目标进程不存在检查 PID/名字
{error, timeout}gen_server:call 超时增加超时或检查服务
bad_return回调返回格式错误检查 {reply, ...} 格式

💡 最佳实践

  1. 所有有状态进程都用 GenServer,不要手写循环
  2. 所有 GenServer 都放在 Supervisor 下
  3. 使用 {local, Name} 注册常驻进程
  4. call 用于查询,cast 用于写入
  5. 处理 handle_info 中的 {'EXIT', ...}'DOWN' 消息

10.8 扩展阅读


上一章:09 - 并发编程 下一章:11 - 监督者详解