gen_statem

gen_statem是Erlang/OTP 19.0引入的新behavior。用于替换gen_fsm

gen_statem支持2种回调模式:

  1. state_functions
    要求状态名StateName必须是原子,并且回调函数的名称与状态同名。这一点与gen_fsm一样。
  2. handle_event_function
    状态名可以是任何形式,此时回调函数为Module:handle_event/4,适用于所有状态下。

回调函数Module:callback_mode()指定gen_statem的回调模式。其返回值为state_functions|handle_event_function|[state_functions, state_enter]|[handle_event_function, state_enter]

callback_mode/0函数的返回列表中加入state_enter,会在每次状态改变的时候调用回调函数,参数为(enter, OldState, ...)调用一次回调函数,即进入状态回调。

behavior函数和回调函数之间的关系如下:

gen_statem module            Callback module
-----------------            ---------------
gen_statem:start
gen_statem:start_link -----> Module:init/1Server start or code change-----> Module:callback_mode/0gen_statem:stop       -----> Module:terminate/3gen_statem:call
gen_statem:cast
erlang:send
erlang:'!'            -----> Module:StateName/3,Module:handle_event/3 <- (state_functions)Module:handle_event/4 <- (handle_event_function)-                     -----> Module:terminate/3-                     -----> Module:code_change/4

可以通过gen_statem:call(ServerRef, Request)gen_statem:call(ServerRef, Request, Timeout)发送一个同步消息即事件{call,From}gen_statem进程。
可以通过gen_statem:cast(ServerRef, Request)发送一个异步消息即事件castgen_statem进程。
可以通过erlang:send()发送一个消息即事件infogen_statem进程。

gen_statem回调函数:

Module:StateName(enter, OldState, Data) -> StateEnterResult(StateName)
Module:StateName(EventType, EventContent, Data) -> StateFunctionResult
Module:handle_event(enter, OldState, StateName, Data) -> StateEnterResult(StateName)
Module:handle_event(EventType, EventContent, StateName, Data) -> HandleEventResult

回调函数返回值格式如下:

StateEnterResult(StateName) ->{next_state, StateName, NewData} |{next_state, StateName, NewData, Actions} |{keep_state, NewData} |{keep_state, NewData, Actions} |keep_state_and_data |{keep_state_and_data, Actions} |{repeat_state, NewData} |{repeat_state, NewData, Actions} |repeat_state_and_data |{repeat_state_and_data, Actions}
StateFunctionResult ->{next_state, NewStateName, NewData} |{next_state, NewStateName, NewData, Actions} |{keep_state, NewData} |{keep_state, NewData, Actions} |keep_state_and_data |{keep_state_and_data, Actions} |{repeat_state, NewData} |{repeat_state, NewData, Actions} |repeat_state_and_data |{repeat_state_and_data, Actions}

其中,Actions表示状态迁移动作,在回调函数返回后指定gen_statem去执行,主要包含:

[postpone |{postpone, true | false} |{next_event, EventType :: event_type(), EventContent :: term()} |hibernate |{hibernate, true | false} |Timeout |{timeout, Time, EventContent} |{timeout, Time, EventContent, Options} |{{timeout, Name}, Time, EventContent} |{{timeout, Name}, Time, EventContent, Options} |{state_timeout, Time, EventContent} |{state_timeout, Time, EventContent, Options} |{reply, From, Reply}]

其中,

需要注意的是,在Actions列表中的后面的Action是会覆盖前面的同类型的Action,例如,列表中的所有事件超时只有最后一个会生效。

EventType事件类型包含:

{call, From} | cast | info | timeout | {timeout, Name} | state_timeout | internal

{call, From}castinfo这3种事件是通过接口产生的外部事件,其他的则是在执行过程中由gen_statem产生的。

一个例子,分别使用两种回调模式实现
  • 回调模式为state_function
-module(pushbutton).
-behaviour(gen_statem).
-export([start/0,push/0,get_count/0,stop/0]).
-export([terminate/3,code_change/4,init/1,callback_mode/0]).
-export([on/3,off/3]).
name() -> pushbutton_statem.
% The registered server name
%% API. This example uses a registered name name()
%% and does not link to the caller.
start() -> gen_statem:start({local,name()}, ?MODULE, [], []).
push() -> gen_statem:call(name(), push).
get_count() -> gen_statem:call(name(), get_count).
stop() -> gen_statem:stop(name()).
%% Mandatory callback functions
terminate(_Reason, _State, _Data) -> void.
code_change(_Vsn, State, Data, _Extra) -> {ok,State,Data}.
init([]) -> %% Set the initial state + data. Data is used only as a counter.State = off, Data = 0, {ok,State,Data}.
callback_mode() -> state_functions.
%%% state callback(s)
off({call,From}, push, Data) -> %% Go to 'on', increment count and reply %% that the resulting status is 'on' {next_state,on,Data+1,[{reply,From,on}]};
off(EventType, EventContent, Data) -> handle_event(EventType, EventContent, Data).
on({call,From}, push, Data) -> %% Go to 'off' and reply that the resulting status is 'off' {next_state,off,Data,[{reply,From,off}]};
on(EventType, EventContent, Data) -> handle_event(EventType, EventContent, Data).
%% Handle events common to all states
handle_event({call,From}, get_count, Data) -> %% Reply with the current count {keep_state,Data,[{reply,From,Data}]};
handle_event(_, _, Data) -> %% Ignore all other events {keep_state,Data}.
  • 回调模式为handle_event_function
-module(pushbutton).
-behaviour(gen_statem).
-export([start/0,push/0,get_count/0,stop/0]).
-export([terminate/3,code_change/4,init/1,callback_mode/0]).
-export([on/3,off/3]).
name() -> pushbutton_statem.
% The registered server name
%% API. This example uses a registered name name()
%% and does not link to the caller.
start() -> gen_statem:start({local,name()}, ?MODULE, [], []).
push() -> gen_statem:call(name(), push).
get_count() -> gen_statem:call(name(), get_count).
stop() -> gen_statem:stop(name()).
%% Mandatory callback functions
terminate(_Reason, _State, _Data) -> void.
code_change(_Vsn, State, Data, _Extra) -> {ok,State,Data}.
init([]) -> %% Set the initial state + data. Data is used only as a counter.State = off, Data = 0, {ok,State,Data}.
callback_mode() -> handle_event_function.
%%% state callback(s)
handle_event({call,From}, push, off, Data) -> %% Go to 'on', increment count and reply %% that the resulting status is 'on' {next_state,on,Data+1,[{reply,From,on}]};
handle_event({call,From}, push, on, Data) -> %% Go to 'off' and reply that the resulting status is 'off' {next_state,off,Data,[{reply,From,off}]};
%% %% Event handling common to all states
handle_event({call,From}, get_count, State, Data) -> %% Reply with the current count {next_state,State,Data,[{reply,From,Data}]};
handle_event(_, _, State, Data) -> %% Ignore all other events {next_state,State,Data}.

官方文档连接:http://erlang.org/doc/design_principles/statem.html
更详细的翻译及讲解可见:https://www.cnblogs.com/-wyp/p/6892632.html#_label2

2021/05/31 补充
  1. OTP 22.3新增可以通过迁移动作{change_callback_module, NewModule}{push_callback_module, NewModule}
    pop_callback_modulegen_statem运行时改变回调模块。上述动作不能在状态进入回调中使用。

  2. 回调模式是回调模块的属性,在服务器启动时设置,并且可以在回调模块改变时改变。

  3. 返回值:repeat_state vs keep_state
    开启进入状态回调的情况下,repeat_state会再次执行进入状态回调,相当于再次进入该状态;而keep_state则不会。

  4. init函数的返回值中使用posepone动作是无效的,因为当前没有任何事件可以延迟。

  5. 状态进入回调并非一个事件,不能在其返回值中改变状态、插入事件、延迟enter事件或者改变回调模块。

  6. 启动相同类型(state_timeout{time_out, Name}timeout)的超时定时器会取消旧的正在运行的定时器,相当于重新开启一个定时器。同名但事件内容不同的定时器相当于同一个定时器。
    OTP 22.1之前,可通过设置一个同类型的超时时间为infinity的定时器来取消超时。
    OTP 22.1之后,可通过{TimoutType, update, NewEventContent}来更新超时事件的内容,通过{TimoutType, cancel}来取消超时定时器。
    零超时计时器不会被启动,但会即刻插入超时事件。

测试代码
  • code_lock.erl
-module(code_lock).
-behaviour(gen_statem).
-define(NAME, code_lock).-export([start_link/1, stop/0]).
-export([down/1, up/1, code_length/0]).
%% Test
-export([change_callback_module/0,push_callback_module/0,pop_callback_module/0,next_state/0,keep_state/0,repeat_state/0,stop/1,stop_and_reply/1,state_timeout/0,timeout/1,timeout_update/1,generic_timeout/3,generic_timeout_update/2,generic_timeout_cancel/1,erlang_timeout/2,erlang_timeout_cancel/1,postpone/1
]).
-export([init/1, callback_mode/0, terminate/3]).
-export([locked/3, open/3]).start_link(Code) ->gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
stop() ->gen_statem:stop(?NAME).down(Button) ->gen_statem:cast(?NAME, {down, Button}).
up(Button) ->gen_statem:cast(?NAME, {up, Button}).
code_length() ->gen_statem:call(?NAME, code_length).%% Just For Test
%% 更改回调模块 Since OTP22.3
change_callback_module() ->gen_statem:cast(?NAME, change_callback_module).
%%% 更改为新的回调模块,将旧的回调模块入栈
push_callback_module() ->gen_statem:cast(?NAME, push_callback_module).
%%% 从栈中取出旧的回调模块并设置为当前回调模块
pop_callback_module() ->gen_statem:cast(?NAME, pop_callback_module).
%% 返回值
%%% next_state vs keep_state vs repeat_state
%%%% 开启进入状态回调的情况下,repeat_state会再次执行进入状态回调,相当于再次进入该状态;而keep_state则不会。
next_state() ->gen_statem:cast(?NAME, next_state).
keep_state() ->gen_statem:cast(?NAME, keep_state).
repeat_state() ->gen_statem:cast(?NAME, repeat_state).
%%% 返回值 stop & stop_and_reply
stop(Reason) ->gen_statem:cast(?NAME, {stop, Reason}).
stop_and_reply(Reason) ->gen_statem:call(?NAME, {stop_and_reply, Reason}).
%% timeout
%%% state_timeout 状态超时
%%%% 按下按钮(down(1)、up(1))后,调用state_timeout,测试是否会覆盖按下按钮后的状态超时{state_timeout, button}
%%%% 结果证明会被覆盖
state_timeout() ->gen_statem:cast(?NAME, state_timeout).
%%% timeout 事件超时
%%%% 执行timeout(20000)、timeout(10000),查看是否覆盖
timeout(MiliS) ->gen_statem:cast(?NAME, {timeout, MiliS}).
%%%% 执行timeout(20000)、timeout_update(aaa),查看内容是否更改
%%%% 测试没有更改,因为此处是事件超时,调用后事件超时就被触发了,无法测试,改用一般超时进行测试
timeout_update(Msg) ->gen_statem:cast(?NAME, {timeout_update, Msg}).
%%% generic_timeout 一般超时
%%%% 执行generic_timeout(aaa, 10000, aaa)、generic_timeout(aaa, 5000, aaa)查看是否覆盖
%%%% 执行generic_timeout(aaa, 10000, aaa)、generic_timeout(bbb, 10000, aaa)查看是否覆盖
generic_timeout(Name, MiliS, Msg) ->gen_statem:cast(?NAME, {generic_timeout, Name, MiliS, Msg}).
%%%% Since OTP22.1
%%%% 执行generic_timeout(aaa, 10000, aaa)、generic_timeout_update(aaa, aaaaa)查看是否更新
generic_timeout_update(Name, Msg) ->gen_statem:cast(?NAME, {generic_timeout_update, Name, Msg}).
%%%% Since OTP22.1
%%%% 执行generic_timeout(aaa, 10000, aaa)、generic_timeout_cancel(aaa)查看是否取消
generic_timeout_cancel(Name) ->gen_statem:cast(?NAME, {generic_timeout_cancel, Name}).
%%% erlang定时器
erlang_timeout(MiliS, Msg) ->gen_statem:cast(?NAME, {erlang_timeout, MiliS, Msg}).
erlang_timeout_cancel(Msg) ->gen_statem:cast(?NAME, {erlang_timeout_cancel, Msg}).
%% postpone事件延迟
%%%% locked状态下调用postpone(aaa)、postpone(bbb),然后在调用next_state(),延迟的事件会再次触发
postpone(Msg) ->gen_statem:cast(?NAME, {postpone, Msg}).
%% next_event插入事件
%%%% 执行down(1)、up(1)
%% hibernation进程挂起init(Code) ->process_flag(trap_exit, true),Data = #{code => Code, length => length(Code), buttons => [], timer => []},{ok, locked, Data}.callback_mode() ->[state_functions, state_enter].-define(HANDLE_COMMON,?FUNCTION_NAME(T, C, D) -> handle_common(T, C, D)).
%% All State Events
handle_common(cast, {down, Button}, Data) ->io:format("code_lock:handle_common cast {down, ~p}~n", [Button]),{keep_state, Data#{button => Button}};
handle_common(cast, {up, Button}, Data) ->io:format("code_lock:handle_common cast {up, ~p}~n", [Button]),case Data of#{button := Button} ->{keep_state, maps:remove(button, Data),[{next_event, internal, {button, Button}}]};#{} ->keep_state_and_dataend;
handle_common(cast, change_callback_module, _Data) ->io:format("code_lock:change_callback_module -> code_lock_2~n", []),{keep_state_and_data, [{change_callback_module, code_lock_2}]};
handle_common(cast, push_callback_module, _Data) ->io:format("code_lock:push_callback_module -> code_lock_2~n", []),{keep_state_and_data, [{push_callback_module, code_lock_2}]};
handle_common(cast, pop_callback_module, _Data) ->io:format("code_lock:pop_callback_module~n", []),{keep_state_and_data, [pop_callback_module]};
handle_common(cast, keep_state, _Data) ->io:format("code_lock:keep_state~n", []),{keep_state, _Data};
handle_common(cast, repeat_state, _Data) ->io:format("code_lock:repeat_state~n", []),{repeat_state, _Data};
handle_common(cast, {stop, Reason}, _Data) ->io:format("code_lock:stop Reson:~p~n", [Reason]),{stop, Reason};
handle_common(cast, {timeout, MiliS}, _Data) ->io:format("code_lock:cast, {timeout, ~p}~n", [MiliS]),{keep_state_and_data, {timeout, MiliS, MiliS}};
handle_common(cast, {timeout_update, Msg}, _Data) ->io:format("code_lock:cast, {timeout_update, ~p}~n", [Msg]),{keep_state_and_data, {timeout, update, Msg}};
handle_common(cast, timeout_cancel, _Data) ->io:format("code_lock:cast, timeout_cancel~n", []),{keep_state_and_data, {timeout, cancel}};
handle_common(cast, {generic_timeout, Name, MiliS, Msg}, _Data) ->io:format("code_lock:cast, generic_timeout Name:~p, MiliS:~p, Msg:~p~n", [Name, MiliS, Msg]),{keep_state_and_data, {{timeout, Name}, MiliS, Msg}};
handle_common(cast, {generic_timeout_update, Name, Msg}, _Data) ->io:format("code_lock:cast, generic_timeout_update Name:~p, Msg:~p~n", [Name, Msg]),{keep_state_and_data, {{timeout, Name}, update, Msg}};
handle_common(cast, {generic_timeout_cancel, Name}, _Data) ->io:format("code_lock:cast, generic_timeout_cancel Name:~p~n", [Name]),{keep_state_and_data, {{timeout, Name}, cancel}};
handle_common(cast, {erlang_timeout, MiliS, Msg}, #{timer := TimerList} = Data) ->io:format("code_lock:cast, erlang_timeout MiliS:~p Msg:~p~n", [MiliS, Msg]),Ref = erlang:start_timer(MiliS, self(), Msg),NewTimerList = lists:keystore(Msg, 1, TimerList, {Msg, Ref}),{keep_state, Data#{timer := NewTimerList}};
handle_common(cast, {erlang_timeout_cancel, Msg}, #{timer := TimerList} = Data) ->io:format("code_lock:cast, erlang_timeout_cancel Msg:~p~n", [Msg]),case lists:keyfind(Msg, 1, TimerList) of{Msg, Ref} ->erlang:cancel_timer(Ref),NewTimerList = lists:keydelete(Msg, 1, TimerList);_ ->NewTimerList = TimerListend,{keep_state, Data#{timer := NewTimerList}};
handle_common({call,From}, {stop_and_reply, Reason}, _Data) ->io:format("code_lock:stop_and_reply Reson:~p~n", [Reason]),{stop_and_reply, Reason, {reply, From, Reason}};
handle_common({call,From}, code_length, #{code := Code}) ->{keep_state_and_data,[{reply,From,length(Code)}]};
handle_common(timeout, Time, _Data) ->io:format("code_lock:timeout Time:~pms~n", [Time]),keep_state_and_data;
handle_common({timeout, Name}, Msg, _Data) ->io:format("code_lock:generic timeout Name:~p Msg:~p~n", [Name, Msg]),keep_state_and_data;
handle_common(info, {timeout, Ref, Msg}, #{timer := TimerList} = Data) ->case lists:member({Msg, Ref}, TimerList) oftrue ->io:format("code_lock:info hit, timeout Msg:~p~n", [Msg]),NewTimerList = lists:keydelete(Msg, 1, TimerList);_ ->io:format("code_lock:info miss, timeout Msg:~p~n", [Msg]),NewTimerList = TimerListend,{keep_state, Data#{timer := NewTimerList}}.%% State: locked
%% bad_state_enter_return_from_state_function
% locked(enter, _OldState, Data) ->
%     io:format("code_lock:enter lock~n", []),
%     do_lock(),
%     {next_state, open, Data};%% 状态进入回调不能改变状态
locked(enter, _OldState, Data) ->io:format("code_lock:enter lock~n", []),do_lock(),{keep_state, Data#{buttons := []}};
locked(state_timeout, button, Data) ->io:format("code_lock:locked state_timeout button~n", []),{keep_state, Data#{buttons := []}};
locked(state_timeout, cast, Data) ->io:format("code_lock:locked state_timeout cast~n", []),{keep_state, Data};
locked(internal, {button,Button},#{code := Code, length := Length, buttons := Buttons} = Data) ->io:format("code_lock:locked internal~n", []),NewButtons =iflength(Buttons) < Length ->Buttons;true ->tl(Buttons)end ++ [Button],ifNewButtons =:= Code -> % Correct{next_state, open, Data};true -> % Incomplete | Incorrect{keep_state, Data#{buttons := NewButtons},[{state_timeout,30000,button}]} %% 每次按下按钮都会重新计时end;
locked(cast, next_state, Data) ->io:format("code_lock:next_state open~n", []),{next_state, open, Data};
locked(cast, state_timeout, Data) ->io:format("code_lock:cast state_timeout locked~n", []),{keep_state, Data, [{state_timeout,10000,cast}]};
locked(cast, {postpone, Msg}, Data) ->io:format("code_lock:cast locked postpone Msg:~p~n", [Msg]),{keep_state, Data, [postpone]};
?HANDLE_COMMON.%% State: open
open(enter, _OldState, _Data) ->io:format("code_lock:enter open~n", []),do_unlock(),{keep_state_and_data,[{state_timeout,10000,lock}]}; % Time in milliseconds
open(state_timeout, lock, Data) ->io:format("code_lock:open state_timeout lock~n", []),{next_state, locked, Data};
open(state_timeout, cast, Data) ->io:format("code_lock:open state_timeout cast~n", []),{next_state, locked, Data};
open(internal, {button,_}, _) ->io:format("code_lock:open internal~n", []),{keep_state_and_data, [postpone]};
open(cast, next_state, Data) ->io:format("code_lock:next_state locked~n", []),{next_state, locked, Data};
open(cast, state_timeout, Data) ->io:format("code_lock:cast state_timeout open~n", []),{keep_state, Data, [{state_timeout,5000,cast}]};
open(cast, {postpone, Msg}, _Data) ->io:format("code_lock:cast open postpone Msg:~p~n", [Msg]),keep_state_and_data;
?HANDLE_COMMON.do_lock() ->io:format("code_lock:Locked~n", []).
do_unlock() ->io:format("code_lock:Open~n", []).terminate(_Reason, State, _Data) ->State =/= locked andalso do_lock(),ok.
  • code_lock_2.erl
-module(code_lock_2).
-behaviour(gen_statem).
-export([init/1, callback_mode/0, terminate/3]).
-export([handle_event/4]).callback_mode() ->[handle_event_function, state_enter].init(Code) ->process_flag(trap_exit, true),Data = #{code => Code, length => length(Code), buttons => []},{ok, locked, Data}.%% State: locked
handle_event(enter, _OldState, locked, Data) ->do_lock(),{keep_state, Data#{buttons := []}};
handle_event(state_timeout, button, locked, Data) ->{keep_state, Data#{buttons := []}};
handle_event(internal, {button,Button}, locked,#{code := Code, length := Length, buttons := Buttons} = Data) ->NewButtons =iflength(Buttons) < Length ->Buttons;true ->tl(Buttons)end ++ [Button],ifNewButtons =:= Code -> % Correct{next_state, open, Data};true -> % Incomplete | Incorrect{keep_state, Data#{buttons := NewButtons},[{state_timeout,30000,button}]} % Time in millisecondsend;%% State: open
handle_event(enter, _OldState, open, _Data) ->do_unlock(),{keep_state_and_data,[{state_timeout,10000,lock}]}; % Time in milliseconds
handle_event(state_timeout, lock, open, Data) ->{next_state, locked, Data};
handle_event(internal, {button,_}, open, _) ->{keep_state_and_data,[postpone]};%% Common events
handle_event(cast, {down,Button}, _State, Data) ->{keep_state, Data#{button => Button}};
handle_event(cast, {up,Button}, _State, Data) ->case Data of#{button := Button} ->{keep_state, maps:remove(button, Data),[{next_event,internal,{button,Button}},{state_timeout,30000,button}]}; % Time in milliseconds#{} ->keep_state_and_dataend;
handle_event(cast, change_callback_module, _State, Data) ->io:format("code_lock_2:change_callback_module -> code_lock~n", []),{keep_state, Data, [{change_callback_module, code_lock}]};
handle_event(cast, push_callback_module, _State, Data) ->io:format("code_lock_2:push_callback_module -> code_lock~n", []),{keep_state, Data, [{push_callback_module, code_lock}]};
handle_event(cast, pop_callback_module, _State, Data) ->io:format("code_lock_2:pop_callback_module~n", []),{keep_state, Data, [pop_callback_module]};
handle_event({call,From}, code_length, _State, #{length := Length}) ->io:format("code_lock_2:code_length~n", []),{keep_state_and_data,[{reply,From,Length}]};
handle_event(Event, EventData, State, Data) ->io:format("code_lock_2:not match Event:~p, EventData:~p, State:~p, Data:~p~n", [Event, EventData, State, Data]),{keep_state_and_data}.do_lock() ->io:format("code_lock_2:Locked~n", []).
do_unlock() ->io:format("code_lock_2:Open~n", []).terminate(_Reason, State, _Data) ->State =/= locked andalso do_lock(),ok.

Erlang -- gen_statem相关推荐

  1. Erlang/OTP设计原则(文档翻译)

    http://erlang.org/doc/design_principles/des_princ.html 图和代码皆源自以上链接中Erlang官方文档,翻译时的版本为20.1. 这个设计原则,其实 ...

  2. Erlang启动参数学习

    项目中脚本里大量使用erlang的启动参数配置,今天来学习一下关于erlang的启动参数 官方API 先贴出官方API的地址 前言 erlang启动参数主要有3种,分别是emulator flag, ...

  3. erlang的tcp服务器模板

    改来改去,最后放github了,贴的也累,蛋疼 还有一个tcp批量客户端的,也一起了 大概思路是 混合模式 使用erlang:send_after添加recv的超时处理 send在socket的opt ...

  4. Erlang服务端开发(无需Erlang基础)笔试题

    某游戏公司Erlang服务端开发(无需Erlang基础)笔试题,面向C/C++程序员 一.用你熟悉的语言解决下面的问题. 1.反转输出字符串,并移除其中的空格. 2.快速的判断一个数是否素数的方法. ...

  5. erlang调优方法

    2019独角兽企业重金招聘Python工程师标准>>> 1. 来自Scaling Erlang的方法 内核调优: # Increase the ipv4 port range: sy ...

  6. CentOS安装新版RabbitMQ解决Erlang 19.3版本依赖

    2019独角兽企业重金招聘Python工程师标准>>> 通过yum等软件仓库都可以直接安装RabbitMQ,但版本一般都较为保守. RabbitMQ官网提供了新版的rpm包(http ...

  7. centos 6.8 源码安装 erlang/otp 19.0.2

    2019独角兽企业重金招聘Python工程师标准>>> 最近一直都是在debian jessie下开发erlang代码,但部署环境一直都是在centos 6上面,发现centos 6 ...

  8. 解析Erlang日志组件lager的lager_transform模块

    为什么80%的码农都做不了架构师?>>>    使用 lager 的时候,在编译应用的时候,需要加入选项 {parse_transform, lager_transform} erl ...

  9. 基于Erlang语言的视频相似推荐系统 | 深度

    作者丨gongyouliu 来源 | 转载自大数据与人工智能(ID:ai-big-data) [导语]:作者在上一篇文章<基于内容的推荐算法>中介绍了基于内容的推荐算法的实现原理.在本篇文 ...

最新文章

  1. 小米豪派大红包!向几千名员工发放股权激励,人均39万,应届生都有!小米员工却吵翻天:不公平,作秀!...
  2. python2.x和3.x为什么不兼容_Python中使用AES算法(解决Python2.x和3.x下运行不兼容问题)...
  3. linux添加后门方法,超初级的linux后门制作方法
  4. C++进阶—— helper function 的设计与实现
  5. PLSQL官方下载、安装和使用完全指南
  6. Linux内核启动过程学习
  7. mysql 事务 库存_库存事务处理临时表
  8. Python深度学习-NLP实战:FastText实现中文文本分类(代码已跑通!)
  9. js获取本月第一天和当前时间
  10. uniapp中使用svga动画
  11. 深度学习baseline模型_深度学习模型在序列标注任务中的应用
  12. AUTOCAD绘制3D家具有感
  13. 川土微电子8通道隔离式数字输入接收器
  14. Arthas-thread命令定位线程死锁
  15. 关于dateadd与datediff的使用方法
  16. 煮酒论语言 -- 曹孟德黑尽天下语言
  17. 杰理之二代手表智能手表方案硬件框架【篇】
  18. 记录--微信小程序,uniapp,H5端发送,显示emoji表情
  19. iPhone下,计算图片的crc
  20. bootstrap动态调用select下拉框

热门文章

  1. C++写纹理贴图中OBJ文件和MTL(材质)文件
  2. Java基于基于Springboot+vue的药品销售商城网站 在线购药 elementui毕业设计
  3. Android 面试题:为什么 Activity 都重建了 ViewModel 还存在?
  4. 抢跑前装量产赛道,这家自动驾驶公司为何要自研域控制器?
  5. 世外桃源六python_关于桃花源的六年级作文:难忘的世外桃源之旅
  6. nova-rootwrap笔记
  7. 皮卡丘的梦想2(线段树+二进制状态压缩)
  8. [日推荐]『准标商标查询』专注商标查询
  9. 项目型公司合理的组织结构
  10. 鸿蒙os系统支持oppo手机吗,华为鸿蒙系统支持的手机型号 鸿蒙OS 2.0支持机型名单...