Erlang OTP 学习笔记

前言

Erlang的巨大优势一部分来自于其并发和分布式特性,还有一部分来自其错误处理能力,那么OTP框架则是第三部分。

OTP简介

OTP(Open Telecom Platform) 是一套用于构建可伸缩、容错和并发应用程序的开发框架和方法论。它是 Erlang 语言的一个重要组成部分,由爱立信公司(Ericsson)开发并开源。

Erlang/OTP 提供了一些预定义的原语,如 gen_server、gen_fsm、gen_event 等,开发者可以基于这些原语构建自定义的进程模型,从而遵循一致的设计模式和最佳实践。

gen_server

先编写一个简单的服务器

-module(kitty_server).
-export([start_link/0, order_cat/4, return_cat/2, close_shop/1]).

-record(cat, {name, color = green, description}).

%%% 客户API
start_link() -> spawn_link(fun init/0).

%% 同步调用
order_cat(Pid, Name, Color, Description) ->
  Ref = erlang:monitor(process, Pid), %%监控器
  Pid ! {self(), Ref, {order, Name, Color, Description}},
  receive
    {Ref, Cat} ->
      erlang:demonitor(Ref, [flush]), %%取消监控
      Cat;
    {'DOWN', Ref, process, Pid, Reason} ->
      erlang:error(Reason)
  after 5000 ->
    erlang:error(timeout)
  end.

%% 这个调用是异步的
return_cat(Pid, Cat = #cat{}) ->
  Pid ! {return, Cat},
  ok.

%% 同步调用
close_shop(Pid) ->
  Ref = erlang:monitor(process, Pid),
  Pid ! {self(), Ref, terminate},
  receive
    {Ref, ok} ->
      erlang:demonitor(Ref, [flush]),
      ok;
    {'DOWN', Ref, process, Pid, Reason} ->
      erlang:error(Reason)
  after 5000 ->
    erlang:error(timeout)
  end.

%%% 服务器函数
init() -> loop([]).

loop(Cats) ->
  receive
    {Pid, Ref, {order, Name, Color, Description}} ->
      if Cats =:= [] ->
        Pid ! {Ref, make_cat(Name, Color, Description)},
        loop(Cats);
        Cats =/= [] -> % 减少库存
          Pid ! {Ref, hd(Cats)},
          loop(tl(Cats))
      end;
    {return, Cat = #cat{}} ->
      loop([Cat | Cats]);
    {Pid, Ref, terminate} ->
      Pid ! {Ref, ok},
      terminate(Cats);
    Unknown ->
      %% 做些日志
      io:format("Unknown message: ~p~n", [Unknown]),
      loop(Cats)
  end.
%%% 私有函数
make_cat(Name, Col, Desc) ->
  #cat{name = Name, color = Col, description = Desc}.

terminate(Cats) ->
  [io:format("~p was set free.~n", [C#cat.name]) || C <- Cats],
  ok.

运行如图:
在这里插入图片描述

我们可以把一些通用组件进行封装。

%%%-------------------------------------------------------------------
-module(my_server).
-compile(export_all).

%%-------------------------------------------------------------
%% 用于启动一个新进程来运行服务器。
%% 它会创建一个新的进程,该进程将执行 init(Module, InitialState) 函数。
%% 服务器的 Module 是一个模块名,而 InitialState 是服务器的初始状态。
%%-------------------------------------------------------------
start(Module, InitialState) ->
  spawn(fun() -> init(Module, InitialState) end).

%%-------------------------------------------------------------
%% 与 start/2 函数类似,
%% 但它使用 spawn_link 函数创建进程,从而在服务器进程和调用者之间建立链接。
%%-------------------------------------------------------------
start_link(Module, InitialState) ->
  spawn_link(fun() -> init(Module, InitialState) end).

%%-------------------------------------------------------------
%% 用于向服务器进程发送同步请求消息,并等待服务器返回响应。
%% 它接收两个参数:Pid 是服务器进程的进程标识符,Msg 是要发送的消息。
%% 函数首先使用 erlang:monitor/2 监视服务器进程,以便在进程退出时能够接收到通知。
%% 然后,它向服务器进程发送一个同步请求消息 {sync, self(), Ref, Msg},
%% 其中 self() 用于获取当前进程的 PID,Ref 是一个参考(reference),用于标识该同步请求消息。
%% 接着,函数使用 receive 语句等待接收服务器的响应。如果收到与参考 Ref 相匹配的响应 {Ref, Reply},则返回响应并执行清理操作。
%% 如果收到了进程退出的通知 {'DOWN', Ref, process, Pid, Reason},则抛出一个异常,将原因 Reason 作为错误信息。
%% 如果在指定的时间(这里是 5000 毫秒)内没有收到任何响应或通知,将抛出一个超时异常。
%%-------------------------------------------------------------
call(Pid, Msg) ->
  Ref = erlang:monitor(process, Pid),
  Pid ! {sync, self(), Ref, Msg}, %%发送一个同步请求消息
  receive
    {Ref, Reply} ->
      erlang:demonitor(Ref, [flush]),
      Reply;
    {'DOWN', Ref, process, Pid, Reason} ->
      erlang:error(Reason)
  after 5000 ->
    erlang:error(timeout)
  end.

%%-------------------------------------------------------------
%% 用于向服务器进程发送异步请求消息。
%% 它接收两个参数:Pid 是服务器进程的进程标识符,
%% Msg 是要发送的消息。函数直接将消息 {async, Msg} 发送给服务器进程,不等待服务器的响应,并返回 ok 表
%%-------------------------------------------------------------
cast(Pid, Msg) ->
  Pid ! {async, Msg},   %%发送一个异步请求消息
  ok.

%%-------------------------------------------------------------
%% 用于在服务器进程中发送响应消息。
%% 当服务器接收到 {sync, Pid, Ref, Msg} 消息时,它可以使用 reply/2 函数向发送进程发送带有参考和响应的消息。
%%-------------------------------------------------------------
reply({Pid, Ref}, Reply) ->
  Pid ! {Ref, Reply}.
%%-------------------------------------------------------------
%% 用于初始化服务器,将服务器的模块和初始状态传递给 loop/2 函数
%%-------------------------------------------------------------
init(Module, InitialState) ->
  loop(Module, Module:init(InitialState)).

%%-------------------------------------------------------------
%% 是服务器的主循环函数,它使用 receive 语句接收消息,并根据消息的类型进行模式匹配和处理。
%% 当接收到 {async, Msg} 消息时,它调用服务器模块的 handle_cast/2 函数处理异步请求。
%% 当接收到 {sync, Pid, Ref, Msg} 消息时,它调用服务器模块的 handle_call/3 函数处理同步请求,并传递参考、发送进程和当前状态。
%%-------------------------------------------------------------
loop(Module, State) ->
  receive
    {async, Msg} ->   %%处理异步请求消息
      loop(Module, Module:handle_cast(Msg, state));
    {sync, Pid, Ref, Msg} ->  %%处理同步请求消息
      loop(Module, Module:handle_call(Msg, {Pid, Ref}, State))
  end.

接下来,只需关注专有部分了

-module(kitty_server2).
-export([start_link/0, order_cat/4, return_cat/2, close_shop/1, handle_cast/2, handle_call/3, init/1]).

-record(cat, {name, color = green, description}).

%%% 客户 API
start_link() -> my_server:start_link(?MODULE, []).

%% 同步调用
order_cat(Pid, Name, Color, Description) ->
  my_server:call(Pid, {order, Name, Color, Description}).

%% 异步调用
return_cat(Pid, Cat = #cat{}) ->
  my_server:cast(Pid, {return, Cat}).


%% 同步调用
close_shop(Pid) ->
  my_server:call(Pid, terminate).


init([]) -> [].

handle_call({order, Name, Color, Description}, From, Cats) ->
  if
    Cats =:= [] ->
      my_server:reply(From, make_cat(Name, Color, Description)),
      Cats;
    true ->
      my_server:reply(From, hd(Cats)),
      tl(Cats)
  end;
handle_call(terminate, From, Cats) ->
  my_server:reply(From, ok),
  terminate(Cats).

handle_cast({return, Cat = #cat{}}, Cats) ->
  [Cat | Cats].

%% 私有函数
make_cat(Name, Col, Desc) ->
  #cat{name = Name, color = Col, description = Desc}.

terminate(Cats) ->
  [io:format("~p was set free.~n", [C#cat.name]) || C <- Cats],
  exit(normal).

上述列子展示了OTP的根本所在:找到所有通用的组件,把它们提炼到库中,保证它们可以良好运行,然后尽可能地重用它们。接下来就只需要关注那些专用部分,也就是那些总是随应用变化而变化的部分

my_server并未实现进程命名、超时配置、调试信息、非期望消息处理、代码热加载的配合、特殊错误的处理、公共回复代码的提炼、服务器关闭的处理、保证服务器和监督者的配合等。

但OTP的gen_server行为中解决了所有这些问题。

init/1 函数

负责初始化服务器的状态,并完成服务器需要的所有一次性任务。这个函数可以返回{ok, State}、{ok, State, TimeOut}、{ok, State, hibernate}、{stop, Reason}以及ignore
常规的1{ok, State}1返回值无需解释,只要记住State会直接传给进程的主循环,并作为进程的状态一直保存在那里就行了。
当期望服务器在某个时间期限之前能收到一条消息时,可以使用TimeOut变量。如果到期没有收到任何消息,那么会给服务器发送一条特殊的消息(原子timeout),可以在handle_info/2中处理这条消息。很少会在产品代码使用这个选项,因为不能总是知道会收到哪条消息,而任意一条消息都会重置计时器。通常,更好的方法是使用erlang:start_timer/3之类的函数,可以获得更好的处理控制。
如果在初始化的过程中出现了错误,可以返回{stop, Reason}

注意!当执行init/1函数时,创建服务器的进程会被阻塞。这是因为它在等待一条“就绪”消息,这条消息由gen_server模块自动发送以确认一切正常

handle_call/3 函数

函数handle_call/3用于处理同步消息。它有3个参数:Request、From以及State。它和我们在my_server中自己实现的handle_call/3非常相似。最大的不同在于回应消息的方式。在my_server的服务器抽象中,必须要使用my_server:reply/2进行回应。在gen_server中,有8种不同的返回值可供选择,这些返回值都是元组形式,具体如下:

  • {reply,Reply,NewState}
  • {reply,Reply,NewState,TimeOut}
  • {reply,Reply,NewState,hibernate}
  • {noreply,NewState}
  • {noreply,NewState,TimeOut}
  • {noreply,NewState,hibernate}
  • {stop,Reason,Reply,NewState}
  • {stop,Reason,NewState}

这些返回值中,TimeOut和hibernate的工作方式和init/1中的一样。Reply中的内容会被原封不动地发回给调用服务器的进程

共有3种不同的noreply选项,当使用noreply时,服务器的通用部分会认为你将自己发送回应消息,可以调用gen_server:reply/2发送回应

在绝大部分情况下,只需要使用reply元组。不过有些情况确实需要使用noreply,例如,希望由另一个进程来替你发送回应,或者想先发送一条确认消息(“嗨!我收到消息了!”),然后继续处理(处理完无需回应)。如果这是所需要的场景,那么只能使用gen_server:reply/2,否则,调用会超时然后崩溃

handle_cast/2 函数

handle_cast/2回调函数的工作方式和my_server中的也很相似。它的参数是Message和State,用于异步消息的处理。和handle_call/3类似,其中也可以进行任何处理。不过,它只能返回noreply元组:

  • {noreply, NewState}
  • {noreply, NewState, TimeOut}
  • {noreply, NewState, hibernate}
  • {stop, Reason, NewState}

handle_info/2 函数

handle_info函数用于处理和接口不相容的消息。它和handle_case/2非常类似,事实上,返回值也完全一样。它们之间的区别在于,这个回调函数只用来处理直接通过!操作符发送的消息,以及如init/1timeout、监控器通知或者EXIT信号之类的特殊消息

terminate/2函数

当上面3种handle_something函数(handle_call/3,handle_cast/2,handle_info/2)返回形如{stop, Reason, NewState}或者{stop, Reason,Reply, NewState}的元组时,会调用terminate/2函数。它有两个参数:Reason和State,分别对应stop元组中的同名字段。
当父进程(创建服务器的进程)死亡时,也会调用terminate/2函数,不过这只会发生在gen_server捕获了退出信号的时候。

如果在调用terminate/2时,原因不是normal、shutdown或者{shutdown, Term},那么OTP框架会把这当成故障,并会把进程的状态、故障原因、最后收到的消息等记入日志。这让调试变得更加容易,可以帮助你快速定位问题

code_change/3函数

函数code_change/3用于代码升级。它的调用形式是code_change(PreviousVersion, State, Extra)。其中,变量PreviousVersion在升级时是版本数据项本身,在降级时(就是加载老代码)是{down, Version}。State变量中保存着服务器当前的所有状态数据,可以对其进行转换。

gen_server实践

使用gen_server来构建kitty_gen_server。它和kitty_server2相似,只在API方面有很少的改变。

-module(kitty_gen_server).
-behavior(gen_server).
-record(cat, {name, color, description}).
-export([start_link/0, order_cat/4, return_cat/2, close_shop/1, init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

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

%%同步调用
order_cat(Pid, Name, Color, Description) ->
  gen_server:call(Pid, {order, Name, Color, Description}).

%%异步调用
return_cat(Pid, Cat = #cat{}) ->
  gen_server:cast(Pid, {return, Cat}).

%%同步调用
close_shop(Pid) ->
  gen_server:call(Pid, terminate).

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

handle_call({order, Name, Color, Description}, _From, Cats) ->
  if
    Cats =:= [] ->
      {reply, make_cat(Name, Color, Description), Cats};
    Cats =/= [] ->
      {reply, hd(Cats), tl(Cats)}
  end;

handle_call(terminate, _From, Cats) ->
  {stop, normal, ok, Cats}.

handle_cast({return, Cat = #cat{}}, Cats) ->
  {noreply, [Cat | Cats]}.

handle_info(Msg, Cats) ->
  io:format("Unexpected message:~p~n", [Msg]),
  {noreply, Cats}.

terminate(normal, Cats) ->
  [io:format("~p was set free.~n", [C#cat.name]) || C <- Cats],
  ok.

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

%%% 私有函数
make_cat(Name, Col, Desc) ->
  #cat{name = Name, color = Col, description = Desc}.

测试如下:

15> c(kitty_gen_server).                     
{ok,kitty_gen_server}
16> rr(kitty_gen_server).
[cat]
17> {_,Pid} = kitty_gen_server:start_link().
{ok,<0.94.0>}
18> Pid ! <<"Test handle info">>.           `测试 hanle_info `                                           
Unexpected message:<<"Test handle info">>
<<"Test handle info">>
19> Cat = kitty_gen_server:order_cat(Pid, "Cat Stevens",white, "not actually a cat").
#cat{name = "Cat Stevens",color = white,
     description = "not actually a cat"}
20> kitty_gen_server:return_cat(Pid, Cat).	`测试handle_cast`
ok
21> kitty_gen_server:order_cat(Pid, "Kitten Mittens",black, "look at them little paws!").	`测试handle_call`
#cat{name = "Cat Stevens",color = white,
     description = "not actually a cat"}
22> kitty_gen_server:order_cat(Pid, "Kitten Mittens",black, "look at them little paws!"). 
#cat{name = "Kitten Mittens",color = black,
     description = "look at them little paws!"}
23> kitty_gen_server:return_cat(Pid, Cat).                                                
ok
24> kitty_gen_server:close_shop(Pid).                                                     
"Cat Stevens" was set free.
ok

gen_fsm

有限状态机(finite-state machine,FSM)是工业界使用的众多重要协议实现的核心部分。使用有限状态机,程序员能够以一种易于理解的方式表达复杂的流程和事件序列。

gen_fsm行为和gen_server有点类似,因为gen_fsmgen_server行为的一个专用版本。它们之间最大的区别在于,gen_fsm中不再处理call消息和cast消息,而是处理同步和异步事件

init函数

FSM中的init函数和通用服务器中使用的init/1完全一样,除了返回值多一些,可接受的返回值为:{ok, StateName, Data}{ok, StateName, Data, Timeout}{ok, StateName, Data, hibernate}以及{stop, Reason}stop元组的工作原理和gen_server中的完全一样,hibernateTimeout的语义也保持不变

StateName是一个新出现的变量。StateName是原子类型,表示下一个被调用的回调函数

StateName函数

函数StateName/2StateName/3是占位名字,由你来决定它们的内容,假设init/1函数返回元组{ok, sitting, Data},这意味着FSM会处于sitting状态

在上面的FSM中,init/1函数的返回值表明我们该处于sitting状态。当gen_fsm进程收到一个事件时,函数sitting/2或者sitting/3会被调用。对于异步事件,会调用sitting/2函数,对于同步事件,会调用sitting/3函数

函数sitting/2(或者一般的说,StateName/2)有两个参数:一个是Event,作为事件发送来的实际消息;一个是StateData,调用携带的数据sitting/2函数可以返回以下几种元组

  • {next_state, NextStateName, NewStateData}
  • {next_state, NextStateName, NewStateData, Timeout}
  • {next_state, NextStateName, hibernate}
  • {stop, Reason, NewStateData}

函数sitting/3的参数与此类似,只是在EventStateData之间多了一个From参数。From参数和gen_fsm:reply/2的用法与gen_server中的完全一样,函数StateName/3可以返回如下元组

  • {reply, Reply, NextStateName, NewStateData}

  • {reply, Reply, NextStateName, NewStateData, Timeout}

  • {reply, Reply, NextStateName, NewStateData, hibernate}

  • {next_state, NextStateName, NewStateData}

  • {next_state, NextStateName, NewStateData, Timeout}

  • {next_state, NextStateName, NewStateData, hibernate}

  • {stop, Reason, Reply, NewStateData}

  • {stop, Reason, NewStateData}

handle_event函数

无论当前在哪个状态中,全局事件都会触发一个特定反应。由于这类事件在每个状态中都会以同样的方式处理,因此handle_event/3回调函数正好满足需要。这个函数的参数和StateName/2类似,不过它在中间多了一个参数StateName,handle_event(Event, StateName, Data),这个参数表明了收到事件时所处的状态。它的返回值和函数StateName/2一样。

handle_syn_event函数

回调函数handle_sync_event/4StateName/3的关系与handle_event/2StateName/2的关系一样。这个函数处理同步全局事件,参数和所返回的元组种类都和StateName/3一样

code_change函数

code_change函数的工作方式和gen_server中的完全一样,只是多出一个额外的状态参数,如code_change(OldVersion, StateName, Data, Extra),并且所返回的元组格式为{ok, NextStateName, NewStateData}。

terminate函数

terminate的行为和通用服务器中的也类似,terminate(Reason, StateName, Data)函数做的工作应该和init/1相反。

gen_fsm实践

在这里插入图片描述

-module(trade_fsm).
-behavior(gen_fsm).
-record(state, {name = "", other, ownitems = [], otheritems = [], monitor, from}).
%% API
-export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4, start/1, start_link/1, trade/2, make_offer/2, ready/1, cancel/1, ask_negotiate/2, accept_negotiate/2, do_offer/2, undo_offer/2, notify_cancel/1, notice/3, unexpected/2, idle/2, idle/3, idle_wait/2, idle_wait/3, add/2, remove/2, negotiate/2, negotiate/3, wait/2, priority/2, ready/2, ready/3]).
-export([retract_offer/2]).

start(Name) ->
  gen_fsm:start(?MODULE, [Name], []).

start_link(Name) ->
  gen_fsm:start_link(?MODULE, [Name], []).

%%请求开始交易会话.当/如果对方接受时放回
trade(OwnPid, OtherPid) ->
  gen_fsm:sync_send_event(OwnPid, {negotiate, OtherPid}, 30000).  %%发送异步消息

%%从列表中选择一个物品进行交易
make_offer(OwnPid, Item) ->
  gen_fsm:send_event(OwnPid, {make_offer, Item}).

%%撤销某个交易物品
retract_offer(OwnPid, Item) ->
  gen_fsm:send_event(OwnPid, {retract_offer, Item}).

%%宣布自己就绪,当对方也宣布时就表明交易完成
ready(OwnPid) ->
  gen_fsm:sync_send_event(OwnPid, ready, infinity).

%%取消交易
cancel(OwnPid) ->
  gen_fsm:sync_send_all_state_event(OwnPid, id, cancel).

%%向另一个FSM发起会话请求
ask_negotiate(OtherPid, OwnPid) ->
  gen_fsm:send_event(OtherPid, {ask_negotiate, OwnPid}).

%%转发玩家的交易接受消息
accept_negotiate(OtherPid, OwnPid) ->
  gen_fsm:send_event(OtherPid, {accept_negotiate, OwnPid}).

%%转发玩家的交易物品提供消息
do_offer(OtherPid, Item) ->
  gen_fsm:send_event(OtherPid, {do_offer, Item}).

%% 转发玩家的交易物品撤销消息
undo_offer(OtherPid, Item) ->
  gen_fsm:send_event(OtherPid, {undo_offer, Item}).

%%取消交易时通知对方
notify_cancel(OtherPid) ->
  gen_fsm:send_all_state_event(OtherPid, cancel).

init(Name) ->
  {ok, idle, #state{name = Name}}.

%% 给玩家发送一条通知,可以是一条发给玩家进程的消息,
notice(#state{name = N}, Str, Args) ->
  io:format("~s: " ++ Str ++ "~n", [N | Args]).

%% 记录非期望的消息
unexpected(Msg, State) ->
  io:format("~p received unknown event ~p while in state ~p~n",
    [self(), Msg, State]).

idle({ask_negotiate, OtherPid}, S = #state{}) ->
  Ref = monitor(process, OtherPid), %%监控器
  notice(S, "~p asked for a trade negotiation", [OtherPid]),
  {next_state, idle_wait, S#state{other = OtherPid, monitor = Ref}};
idle(Event, Data) ->
  unexpected(Event, idle),
  {next_state, idle, Data}.
idle({negotiate, OtherPid}, From, S = #state{}) ->
  ask_negotiate(OtherPid, self()),
  notice(S, "asking user ~p for a trade", [OtherPid]),
  Ref = monitor(process, OtherPid),
  {next_state, idle_wait, S#state{other = OtherPid, monitor = Ref, from = From}};
idle(Event, _From, Data) ->
  unexpected(Event, idle),
  {next_state, idle, Data}.

idle_wait({ask_negotiate, OtherPid}, S = #state{other = OtherPid}) ->
  gen_fsm:reply(S#state.from, ok),
  notice(S, "starting negotiation", []),
  {next_state, negotiate, S};
%% 对方接受了我们的请求,迁移到negotiate状态。
idle_wait({accept_negotiate, OtherPid}, S = #state{other = OtherPid}) ->
  gen_fsm:reply(S#state.from, ok),
  notice(S, "starting negotiation", []),
  {next_state, negotiate, S};
idle_wait(Event, Data) ->
  unexpected(Event, idle_wait),
  {next_state, idle_wait, Data}.
idle_wait(accept_negotiate, _From, S = #state{other = OtherPid}) ->
  accept_negotiate(OtherPid, self()),
  notice(S, "accepting negotiation", []),
  {reply, ok, negotiate, S};
idle_wait(Event, _From, Data) ->
  unexpected(Event, idle_wait),
  {next_state, idle_wait, Data}.

%% 向物品列表中增加一件物品
add(Item, Items) ->
  [Item | Items].

%% 从物品列表中移除一件物品
remove(Item, Items) ->
  Items -- [Item].

negotiate({make_offer, Item}, S = #state{ownitems = OwnItems}) ->
  do_offer(S#state.other, Item),
  notice(S, "offering ~p", [Item]),
  {next_state, negotiate, S#state{ownitems = add(Item, OwnItems)}};
%% 本方撤销一件交易物品
negotiate({retract_offer, Item}, S = #state{ownitems = OwnItems}) ->
  undo_offer(S#state.other, Item),
  notice(S, "cancelling offer on ~p", [Item]),
  {next_state, negotiate, S#state{ownitems = remove(Item, OwnItems)}};
%% 对方提供一件交易物品
negotiate({do_offer, Item}, S = #state{otheritems = OtherItems}) ->
  notice(S, "other player offering ~p", [Item]),
  {next_state, negotiate, S#state{otheritems = add(Item, OtherItems)}};
%% 对方撤销一件交易物品
negotiate({undo_offer, Item}, S = #state{otheritems = OtherItems}) ->
  notice(S, "Other player cancelling offer on ~p", [Item]),
  {next_state, negotiate, S#state{otheritems = remove(Item, OtherItems)}};
negotiate(are_you_ready, S = #state{other = OtherPid}) ->
  io:format("Other user ready to trade.~n"),
  notice(S,
    "Other user ready to transfer goods:~n"
    "You get ~p, The other side gets ~p",
    [S#state.otheritems, S#state.ownitems]),
  not_yet(OtherPid),
  {next_state, negotiate, S};
negotiate(Event, Data) ->
  unexpected(Event, negotiate),
  {next_state, negotiate, Data}.
negotiate(ready, From, S = #state{other = OtherPid}) ->
  are_you_ready(OtherPid),
  notice(S, "asking if ready, waiting", []),
  {next_state, wait, S#state{from = From}};
negotiate(Event, _From, S) ->
  unexpected(Event, negotiate),
  {next_state, negotiate, S}.

wait({do_offer, Item}, S = #state{otheritems = OtherItems}) ->
  gen_fsm:reply(S#state.from, offer_changed),
  notice(S, "other side offering ~p", [Item]),
  {next_state, negotiate, S#state{otheritems = add(Item, OtherItems)}};
wait({undo_offer, Item}, S = #state{otheritems = OtherItems}) ->
  gen_fsm:reply(S#state.from, offer_changed),
  notice(S, "Other side cancelling offer of ~p", [Item]),
  {next_state, negotiate, S#state{otheritems = remove(Item, OtherItems)}};
wait(are_you_ready, S = #state{}) ->
  am_ready(S#state.other),
  notice(S, "asked if ready, and I am. Waiting for same reply", []),
  {next_state, wait, S};
wait(are_you_ready, S = #state{}) ->
  am_ready(S#state.other),
  notice(S, "asked if ready, and I am. Waiting for same reply", []),
  {next_state, wait, S};
wait('ready!', S = #state{}) ->
  am_ready(S#state.other),
  ack_trans(S#state.other),
  gen_fsm:reply(S#state.from, ok),
  notice(S, "other side is ready. Moving to ready state", []),
  {next_state, ready, S};
%% 不关心这些消息!
wait(Event, Data) ->
  unexpected(Event, wait),
  {next_state, wait, Data}.

priority(OwnPid, OtherPid) when OwnPid > OtherPid -> true;
priority(OwnPid, OtherPid) when OwnPid < OtherPid -> false.

ready(ack, S = #state{}) ->
  case priority(self(), S#state.other) of
    true ->
      try
        notice(S, "asking for commit", []),
        ready_commit = ask_commit(S#state.other),
        notice(S, "ordering commit", []),
        ok = do_commit(S#state.other),
        notice(S, "committing...", []),
        commit(S),
        {stop, normal, S}
      catch Class:Reason ->
        %% 退出! ready_commit 或者 do_commit 失败了
        notice(S, "commit failed", []),
        {stop, {Class, Reason}, S}
      end;
    false ->
      {next_state, ready, S}
  end;
ready(Event, Data) ->
  unexpected(Event, ready),
  {next_state, ready, Data}.
ready(ask_commit, _From, S) ->
  notice(S, "replying to ask_commit", []),
  {reply, ready_commit, ready, S};
ready(do_commit, _From, S) ->
  notice(S, "committing...", []),
  commit(S),
  {stop, normal, ok, S};
ready(Event, _From, Data) ->
  unexpected(Event, ready),
  {next_state, ready, Data}.
commit(S = #state{}) ->
  io:format("Transaction completed for ~s. "
  "Items sent are:~n~p,~n received are:~n~p.~n"
  "This operation should have some atomic save "
  "in a database.~n",
    [S#state.name, S#state.ownitems, S#state.otheritems]).
%% 对方玩家发送了取消事件
%% 停止正在做的工作,终止进程
handle_event(cancel, _StateName, S = #state{}) ->
  notice(S, "received cancel event", []),
  {stop, other_cancelled, S};
handle_event(Event, StateName, Data) ->
  unexpected(Event, StateName),
  {next_state, StateName, Data}.
%% 本方玩家取消了交易。必须通知对方玩家我们退出了
handle_sync_event(cancel, _From, _StateName, S = #state{}) ->
  notify_cancel(S#state.other),
  notice(S, "cancelling trade, sending cancel event", []),
  {stop, cancelled, ok, S};
%% 注意:不要回复非期待的消息,让调用者崩溃
handle_sync_event(Event, _From, StateName, Data) ->
  unexpected(Event, StateName),
  {next_state, StateName, Data}.

handle_info({'DOWN', Ref, process, Pid, Reason}, _, S = #state{other = Pid, monitor = Ref}) ->
  notice(S, "Other side dead", []),
  {stop, {other_down, Reason}, S};
handle_info(Info, StateName, Data) ->
  unexpected(Info, StateName),
  {next_state, StateName, Data}.

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

%% 交易结束。
terminate(normal, ready, S = #state{}) ->
  notice(S, "FSM leaving.", []);
terminate(_Reason, _StateName, _StateData) ->
  ok.

解释:

  • start/1 和 start_link/1:这些函数用于启动一个交易有限状态机(FSM)进程。它们会调用 gen_fsm:start/3 或 gen_fsm:start_link/3 函数,传递模块名和参数列表。
  • trade/2:这个函数用于请求开始一个交易会话。它会发送一个异步消息给自己的 FSM 进程,并指定另一个参与交易的进程的 PID。
  • make_offer/2:这个函数用于向交易会话中添加一个交易物品。它会发送一个消息给自己的 FSM 进程,包含要添加的物品信息。
  • retract_offer/2:这个函数用于撤销一个交易物品。它会发送一个消息给自己的 FSM 进程,指定要撤销的物品信息。
  • ready/1:这个函数用于宣布自己已经准备好进行交易。它会发送一个同步消息给自己的 FSM 进程,等待对方也宣布准备好交易。
  • cancel/1:这个函数用于取消交易。它会发送一个同步消息给自己的 FSM 进程,通知取消交易。
  • ask_negotiate/2 和 accept_negotiate/2:这些函数用于向另一个 FSM 进程发起或接受交易会话请求。
  • do_offer/2 和 undo_offer/2:这些函数用于转发交易物品的提供或撤销消息给另一个 FSM 进程。
  • notify_cancel/1:这个函数用于通知对方玩家交易已取消。
  • init/1:这个函数是 FSM 的初始化函数,用于初始化状态和存储相关的数据。
  • notice/3:这个函数用于向玩家发送通知。
  • unexpected/2:这个函数用于处理接收到的未知消息的情况,打印出相关信息。
  • idle/2 和 idle/3:这些函数处理 FSM 的 “idle” 状态,包括处理交易请求和状态迁移。
  • idle_wait/2 和 idle_wait/3:这些函数处理 FSM 的 “idle_wait” 状态,等待对方玩家的响应和状态迁移。
  • add/2 和 remove/2:这些函数用于向物品列表中添加或移除物品。
  • negotiate/2 和 negotiate/3:这些函数处理 FSM 的 “negotiate” 状态,处理交易物品的提供、撤销和确认操作。

gen_event

gen_event行为与gen_server以及gen_fsm有很大的不同,它根本不需要实际启动一个进程。之所以不需要进程是因为它的工作方式是“接受一组回调函数”。

简单来讲,gen_event行为运行这个接受并调用回调函数的事件管理器进程,而你只需要提供包含这些回调函数的模块即可。这意味着你无需关心事件分派,只需按照事件管理器要求的格式放置回调函数即可。

init和terminate函数

init和terminate函数与我们前面看到的gen_server和gen_fsm行为中的类似。init/1函数接收列表参数,返回{ok,State}。在init/1中创建的东西,要在terminate/2函数中有对应释放操作。

handle_event函数

handle_event(Event,State)函数可以说是gen_event回调模块的核心函数。和gen_server中的hanle_cast/2一样,handle_event/2函数也是异步的。不过,它的返回值有所不同:

  • {ok, NewState};
  • {ok, NewState, hibernate},让事件管理器进程进入休眠状态,直到收到下一个事件;
  • remove_handler;
  • {swap_handler, Args1, NewState, NewHandler, Args2}。

返回值{ok, NewState}元组的含义和gen_server:handle_cast/2函数中的一样。它只更新自己的状态,不做任何回应。返回{ok, NewState, hibernate}则会使整个事件管理器进入休眠状态。

返回remove_handler则会导致事件处理器从事件管理器中删除。当某个事件处理器知道自己已经完成工作并且无其他任务时,可以使用这个返回值。
最后一个是返回值{swap_handler, Args1, NewState, NewHandler, Args2},这个返回值不太常用。它移除当前事件处理器并用一个新的替代它。

handle_call函数

handle_call函数和gen_server的handle_call回调函数类似,不同之处在于,它可以返回{ok, Reply, NewState}、{ok, Reply, NewState,hibernate}、{remove_handler, Reply}以及{swap_handler, Reply, Args1, NewState, Handler2, Args2}。使用gen_event:call/3-4函数可以发起该调用。

handle_info函数

handle_info回调和hanle_event回调非常类似(有着同样的返回值和含义),唯一的不同在于,handle_info只处理带外消息,如退出信号或使用!操作符直接向事件管理器进程发送消息。它的使用场景和gen_server以及gen_fsm中handle_info的使用场景类似。

code_change函数

code_change函数的工作方式和gen_server中的一样,不过它仅仅针对单独的事件处理器。它的参数为OldVsn、State、Extra,分别表示版本号、当前事件处理器的状态,最后这个参数——Extra,目前可以不用关心。这个方法只要返回{ok, NewState}即可。

gen_event实践

简单实践,模拟比赛积分板
curling_scoreboard_hw.erl

-module(curling_scoreboard_hw).

%% API
-export([add_point/1, next_round/0, set_teams/2, reset_board/0]).


%% 在记分牌上显示参赛队伍
set_teams(TeamA, TeamB) ->
    io:format("Scoreboard: Team ~s vs. Team ~s~n", [TeamA, TeamB]).

%%下一回合
next_round() ->
    io:format("Scoreboard: round over~n").

%%增加点数
add_point(Team) ->
    io:format("Scoreboard: increased score of team ~s by 1~n", [Team]).

%%复位
reset_board() ->
    io:format("Scoreboard: All teams are undefined and all scores are 0~n").

使用gen_event回调模块
curling_scoreboard.erl

-module(curling_scoreboard).
-behavior(gen_event).

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

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

handle_event({set_teams, TeamA, TeamB}, State) ->
  curling_scoreboard_hw:set_teams(TeamA, TeamB),
  {ok, State};
handle_event({add_points, Team, N}, State) ->
  [curling_scoreboard_hw:add_point(Team) || _ <- lists:seq(1, N)],
  {ok, State};
handle_event(next_round, State) ->
  curling_scoreboard_hw:next_round(),
  {ok, State};
handle_event(_, State) ->
  {ok, State}.

handle_call(_, State) ->
  {ok, ok, State}.

handle_info(_, State) ->
  {ok, State}.

terminate(_Args, _State) ->
  ok.

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

测试

1> c(curling_scoreboard_hw).
{ok,curling_scoreboard_hw}
2> c(curling_scoreboard).
{ok,curling_scoreboard}
3> {ok, Pid} = gen_event:start_link().
{ok,< 0.118.0>}
4> gen_event:add_handler(Pid, curling_scoreboard, []).
ok
5> gen_event:notify(Pid, {set_teams, “Pirates”, “Scotsmen”}).
Scoreboard: Team Pirates vs. Team Scotsmen
ok
6> gen_event:notify(Pid, {add_points, “Pirates”, 3}).
ok
Scoreboard: increased score of team Pirates by 1
Scoreboard: increased score of team Pirates by 1
Scoreboard: increased score of team Pirates by 1
7> gen_event:notify(Pid, next_round).
Scoreboard: round over
ok
8> gen_event:delete_handler(Pid, curling_scoreboard, turn_off).
ok
9> gen_event:notify(Pid, next_round).
Ok

监督者(Supervisor)

监督者中的概念

如果把监督者定义为一个除了保证在其子进程死亡后重启它们之外,不做其他任何事情的进程,那么工作者就是那些负责完成实际的工作,并且在工作的过程中可能会死掉的进程。工作者进程通常都被认为是不可靠的。监督者可以监督工作者和其他监督者。工作者只能放置在一个监督者下面
为什么每个进程都要被监督呢?如果创建了未受监督的进程,如何才能确认它们死了呢?如果某个东西无法被度量,那么它就是不存在。

使用监督者

监督者使用起来很简单。我们只需要提供一个回调函数:init/1。但这个函数的返回值非常复杂。一般形式定义为
{ok, {{RestartStrategy, MaxRestart, MaxTime},[ChildSpec]}}.

重启策略

上述定义中的RestartStrategy的值可以为one_for_one、one_for_all、 rest_for_one以及simple_one_for_one。

  1. one_for_one: 如果监督者监督了很多工作者,当其中的一个工作者失败时,只需重启这个工作者即可
  2. one_for_all:当所有的工作者进程都受同一个监督者监督,且这些工作者进程必须互相依赖才能正常工作时,就使用这个策略
  3. rest_for_one:rest_for_one是一个更加特殊的策略。当需要启动一组进程,而这些进程互相依赖形成一条链(A启动B,B启动C,C启动D,以此类推)时,可以使用rest_for_one。
  4. simple_one_for_one:这种类型的监督者只监督一种子进程,当希望以动态的方式向监督者中增加子进程,而不是静态启动子进程时,可以使用这种策略。
重启限制

RestartStrategy元组中剩余的两个变量是MaxRestart和MaxTime。它们的意思是,如果在MaxTime(以秒为单位)指定的时间内,重启次数超过了MaxRestart指定的数字,那么监督者会放弃重启并终止所有子进程,然后自杀,永远停止运行。

子进程规格说明

子进程的规格说明可用描述如下:
{ChildId, StartFunc, Restart, Shutdown, Type, Modules}.

  1. ChildId:ChildId只是监督者内部使用的一个名称。除了在调试或者想获取监督者的所有子进程列表时,这个名字会比较有用之外,基本上没有其他用途。
  2. StartFunc: StartFunc是一个元组,用来指定子进程的启动方式。它采用了标准的{M,F,A}格式
  3. Restart: Restart指定了监督者在某个特定的子进程死后的处理方式,它可以取如下3个值:永久(permanent),永久(permanent),暂态(transient)
  4. Shutdown:用来指定终止的超时期限
  5. Type:Type字段可以让监督者知道子进程是一个监督者(supervisor)(实现了supervisor或者supervisor_bridge行为)还是一个工作者(worker)(其他任何OTP进程)
  6. Modules:Modules是一个列表,其中只有一个元素:子进程行为使用的回调模块名

监督者实践

使用gen_sever编写程序模拟每个乐队成员

-module(musicians).
-behavior(gen_server).

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

-record(state, {name = "", role, skill = good}).
-define(DELAY, 750).

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

stop(Role) -> gen_server:call(Role, stop).

init([Role, Skill]) ->
  %% 这样,就可以知道父进程何时终止
  process_flag(trap_exit, true),  %%进程捕获其他进程发送的终止信号,以便进行自定义的处理
  %% 设定进程生存期内随机数生成使用的种子
  %% 使用当前时间。now()可以保证值唯一
  rand:seed(now()),
  TimeToPlay = rand:uniform(3000),
  Name = pick_name(),
  StrRole = atom_to_list(Role),
  io:format("Musician ~s, playing the ~s entered the room~n",
    [Name, StrRole]),
  {ok, #state{name = Name, role = StrRole, skill = Skill}, TimeToPlay}.

pick_name() ->
  %% 使用随机函数时,必须设置种子。
  %% 请在调用了init/1函数的进程中使用
  lists:nth(random:uniform(10), firstnames())
  ++ " " ++
    lists:nth(random:uniform(10), lastnames()).

firstnames() ->
  ["Valerie", "Arnold", "Carlos", "Dorothy", "Keesha",
    "Phoebe", "Ralphie", "Tim", "Wanda", "Janet"].

lastnames() ->
  ["Frizzle", "Perlstein", "Ramon", "Ann", "Franklin",
    "Terese", "Tennelli", "Jamal", "Li", "Perlstein"].

handle_call(stop, _From, S = #state{}) ->
  {stop, normal, ok, S};
handle_call(_Message, _From, S) ->
  {noreply, S, ?DELAY}.

handle_cast(_Message, S) ->
  {noreply, S, ?DELAY}.

handle_info(timeout, S = #state{name = N, skill = good}) ->
  io:format("~s produced sound!~n", [N]),
  {noreply, S, ?DELAY};
handle_info(timeout, S = #state{name = N, skill = bad}) ->
  case random:uniform(5) of
    1 ->
      io:format("~s played a false note. Uh oh~n", [N]),
      {stop, bad_note, S};
    _ ->
      io:format("~s produced sound!~n", [N]),
      {noreply, S, ?DELAY}
  end;
handle_info(_Message, S) ->
  {noreply, S, ?DELAY}.

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

terminate(normal, S) ->
  io:format("~s left the room (~s)~n", [S#state.name, S#state.role]);
terminate(bad_note, S) ->
  io:format("~s sucks! kicked that member out of the band! (~s)~n",
    [S#state.name, S#state.role]);
terminate(shutdown, S) ->
  io:format("The manager is mad and fired the whole band! "
  "~s just got back to playing in the subway~n",
    [S#state.name]);
terminate(_Reason, S) ->
  io:format("~s has been kicked out (~s)~n", [S#state.name, S#state.role]).

每当服务器设置的超时到了,音乐人就会弹奏一个音符。如果这个音乐人的技能不错,那么一切完美。如果这个音乐人的技能水平糟糕,那它可能有五分之一的机会弹错一个音符,导致自己崩溃。在每个非终止调用的末尾也都设置了?DELAY时间的超时。
实现乐队监督者

-module(band_supervisor).
-behavior(supervisor).

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

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

%% 基于乐队监督者的情绪类型设定在解散乐队前所允许的犯错次数
%% 宽容型监督者对错误的容忍度要高于爱发火型的
%% 爱发火型监督者对于错误的容忍度要高于暴脾气型的监督者
init(lenient) ->  %%宽容型
init({one_for_one, 3, 60});
init(angry) ->  %%爱发火型
init({rest_for_one, 2, 60});
init(jerk) -> %%暴脾气型
init({one_for_all, 1, 60});

init({RestartStrategy, MaxRestart, MaxTime}) ->
{ok, {{RestartStrategy, MaxRestart, MaxTime},
  [{singer,
    {musicians, start_link, [singer, good]},
    permanent, 1000, worker, [musicians]},
    {bass,
      {musicians, start_link, [bass, good]},
      temporary, 1000, worker, [musicians]},
    {drum,
      {musicians, start_link, [drum, bad]},
      transient, 1000, worker, [musicians]},
    {keytar,
      {musicians, start_link, [keytar, good]},
      transient, 1000, worker, [musicians]}
  ]}}.

测试

1> band_supervisor:start_link(lenient).
Musician Carlos Terese, playing the singer entered the room
Musician Carlos Tennelli, playing the bass entered the room
Musician Arnold Tennelli, playing the drum entered the room
Musician Arnold Tennelli, playing the keytar entered the room
{ok,<0.57.0>}
2> Carlos Terese produced sound!
2> Carlos Terese produced sound!

=ERROR REPORT==== 4-Jul-2023::21:43:54 ===
** Generic server drum terminating
** Last message in was timeout
** When Server state == {state,“Valerie Franklin”,“drum”,bad}
** Reason for termination ==
** bad_note
2> Musician Carlos Ann, playing the drum entered the room
2> Carlos Tennelli produced sound!
2> Arnold Tennelli produced sound!

2> Carlos Ann played a false note. Uh oh
2> Carlos Ann sucks! kicked that member out of the band! (drum)
=ERROR REPORT==== 4-Jul-2023::21:44:01 ===
** Generic server drum terminating
** Last message in was timeout
** When Server state == {state,“Carlos Ann”,“drum”,bad}
** Reason for termination ==
** bad_note
2> The manager is mad and fired the whole band! Arnold Tennelli just got back to playing in the subway
2> The manager is mad and fired the whole band! Carlos Tennelli just got back to playing in the subway
2> The manager is mad and fired the whole band! Carlos Terese just got back to playing in the subway
2> 2> 2> ** exception error: shutdown

可以看到,一开始只有鼓手被开除了,过了一会儿,乐队中的每个成员都被开除了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值