ETS 和 gen_server 的使用

一:ETS

1.表的类型

ETS DETS 表保存的是元组。元组里的某一个元素(默认是第一个)被称为该表的 。通过键来向表里插入和提取元组。当我们向表里插入一个元组时会发生什么,取决于表的类型和键的值。一些表被称为异键表 set ,它们要求表里所有的键都是唯一的。另一些被称为 同键表bag ,它们允许多个元素拥有相同的键
ets有4种类型的table,如表所示:  
settable中的每一个Value(Tuple)都是唯一,并且一个Key只能对应一个Value
ordered_set同set,唯一不同的是table中的Key是有序的
bagtable中的每一个Value都是唯一,但是一个key可以对应多个Value
duplicate_bagtable中每一个key可以对应多个Value,并且允许Value重复
基本的表类型(异键表和同键表)各有两个变种,它们共同构成四种表类型:异键、有序异键(ordered set )、同键和副本同键( duplicate bag )。在异键表里,各个元组里的键都必须是独一 无二的。
可以用下面这个小测试程序来演示它们是如何工作的:
%% ets_test.erl
-module(ets_test).
-export([start/0]).

start() ->
    lists:foreach(fun test_ets/1,
                  [set, ordered_set, bag, duplicate_bag]).

test_ets(Mode) ->
    Tableld = ets:new(test, [Mode]),
    ets:insert(TableId, {a,1}),
    ets:insert(TableId, {b,2}),
    ets:insert(TableId, {a,1}),
    ets:insert(TableId, {a,3}),
    List = ets:tab2list(TableId),
    io:format ("~-13w =>~p~n", [Mode, List]),
    ets:delete(TableId).
这个程序会为种模式各创建一个ETS 表,然后向表内插入 {a,1} {b,2} {a,1} 和{a,3四}。
然后调用 tab2list ,它会把整个表转换成一个列表并打印出来。运行这个程序会得到以下输出:
1>ets_test:start().
set            => [{b,2}, {a,3}]
ordered_set    => [{a,3}, {b,2}]
bag            => [{b,2}, {a,1}, {a,3}]
duplicate_bag  => [{b,2}, {a,1}, {a,1}, {a,3}]
在异键型的表里,每个键都只能出现一次。如果先向表内插入元组 {a,1} 然后再插入 {a,3} ,那么最终值将是{a,3} 。异键表和有序异键表的唯一区别是有序异键表里的元素会根据它们的键排序。用tab2list 把表转换成列表之后就能看到这一顺序了。

2.创建一个 ETS

创建 ETS 表的方法是调用 ets:new 。创建表的进程被称为该表的 主管 。创建表时所设置的一
组选项在以后是无法更改的。如果主管进程挂了,表的空间就会被自动释放。你可以调用
ets:delete 来删除表。
ets:new 的参数如下:

-spec ets:new(Name, [Opt]) -> TableId
其中, Name 是一个原子。 [Opt] 是一列选项,源于下面这些参数。
set | ordered_set | bag | duplicate_bag
创建一个指定类型的 ETS 表(我们之前讨论过)。
private
创建一个私有表,只有主管进程才能读取和写入它。
public
创建一个公共表,任何知道此表标识符的进程都能读取和写入它。
protected
创建一个受保护表,任何知道此表标识符的进程都能读取它,但只有主管进程才能写
入它。
named_table
如果设置了此选项, Name 就可以被用于后续的表操作。
{keypos, K}
K 作为键的位置。通常键的位置是 1 。基本上唯一需要使用这个选项的场合是保存 Erlang
记录(它其实是元组的另一种形式),并且记录的第一个元素包含记录名的时候。

 3.ETS示例程序

这次的示例是关于生成三字母组合( trigram)的。这是一个不错的“表演性”程序,它演示了 ETS 表的威力。
(1)三字母组合迭代函数:
定义一个名为 for_each_trigram_in_the_english_language(F, A) 的函数。这个函数会把fun F 应用到英语里的每一个三字母组合上。 F 是一个类型为 fun(Str, A) -> A fun Str 涵盖了英语里所有的三字母组合,A 则是一个累加器。要编写这个迭代函数,需要一个庞大的单词表。(注意:我在这里称它为迭代函数,但是更严格来说,它其实是一个很像lists:foldl 的折叠操作符。)我使用了一个包含 354984 个英语单 词的集合② 来生成三字母组合。利用这个单词表,可以定义三字母组合的迭代函数如下:
%% lib_trigrams.erl
for_each_trigram_in_the_english_language(F, A0) ->
    {ok,Bin0} = file:read_file("354984si.ngl.gz"),
    Bin = zlib:gunzip(Bin0),
    scan_word_list(binary_to_list(Bin), F, A0).

scan_word_list([], _, A) ->
    A;
scan_word_list(L, F, A) ->
    {word, L1} = get_next_word(L, []),
    A1 = scan_trigrams([$\s | Word], F, A),
    scan_word_list(L1, F, A1).

%% 扫描单词,寻找\r\n。
%% 第二个参数是(反转的)单词,
%% 所以必须在找到\r\n或扫描完字符时把它反转回来

get_next_word([$\r,$\n|T], L) -> {reverse([$\s|L]), T};
get_next_word([H|T], L) -> get_next_word(T, [H|L]);
get_next_word([], L) -> {reverse([$\s|L]), []}.

scan_trigrams([X, Y, Z], F, A) ->
    F([X, Y, Z], A);
scan_trigrams([X, Y, Z|T], F, A) ->
    A1 = F([X, Y, Z], A),
    scan_trigrams([Y, Z|T], F, A1);
scan_trigrams(_, _, A) ->
    A.
这里有两点要注意。首先,用 zlib:gunzip(Bin) 解压缩源文件里的二进制数据。这个单词表相当长,所以我们更希望让它以压缩文件的形式保存在磁盘上,而不是原始的ASCII 文件。其次,在每个单词的前后各加了一个空格。进行三字母组合分析时,我们希望把空格当成一个普通字母。
(2)创建一些表:我们将像这样创建 ETS 表:
%% lib_trigrams.erl
make_ets_ordered_set() -> make_a_set(ordered_set, "trigramsos.tab").
make_ets_set()         -> make_a_set(set, "trigramss.tab").

make_a_set(Type, FileName) ->
    Tab = ets:new(table, [Type]),
    F = fun(Str, _) -> ets:insert(Tab, {list_to_binary(Str)}) end,
    for_each_trigram_in_the_english_language(F,0),
    ets:tab2file(Tab, FileName),
    Size = ets:info(Tab, size),
    ets:delete(Tab),
    Size.
请注意,当我们分离出一个三字母组合 ABC 时,实际上是把元组 {<<"ABC">>} 插入了代表各种三字母组合的ETS 表里。这看起来很滑稽:元组里只有 一个 元素。元组通常是多个元素的容器,所以让元组只包含一个元素是违反常识的。但是请记住,ETS表里的所有条目都是元组,而且在默认情况下元组的键就是它的第一个元素。所以对我们来说,{Key}代表了一个没有值的键。接下来的代码将创建一个包含所有三字母组合的异键表(这次用的是Erlang模块sets,而不是ETS):
%% lib_trigrams.erl
make_mod_set() ->
    D = sets:new(),
    F = fun(Str, Set) -> sets:add_element(list_to_binary(Str), Set) end,
    D1 = for_each_trigram_in_the_english_language(F, D),
    file:write_file("trigrams.set", [term_to_binary(D1)]).

二:gen_server

1.gen_server入门

(1)确定回调模块名:我们将制作一个简单的支付系统。把这个模块称为my_bank(我的银行)。

(2)编写接口方法:我们将定义五个接口方法,它们都在my_bank模块里,分别是:

start()
打开银行。
stop()
关闭银行。
new account(Who)
创建一个新账户。
deposit (Who,Amount)
把钱存人银行。
withdraw(Who,Amount)
把钱取出来(如果有结余的话)。

每个函数都正好对应一个gen_server方法调用,代码如下:

%% my_bank.erl
start() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
stop() -> gen_server:call(?MODULE, stop).

new_account(Who) -> gen_server:call(?MODULE, {new, Who}).

deposit(Who, Amount) -> gen_server:call(?MODULE, {add, Who, Amount}).

withdraw(Who, Amount) -> gen_server:call(?MODULE, {remove, Who, Amount}).
gen_server:start_link({local, Name}, Mod, ...) 会启动一个 本地 服务器。如果第一个参数是原子global ,它就会启动一个能被 Erlang 节点集群访问的全局服务器。 start_link 的第二个参数Mod ,也就是回调模块名。宏 ?MODULE 会展开成模块名 my_bank 。目前我们将忽略 gen_server:start_link的其他参数。gen_server:call(?MODULE, Term)被用来对服务器进行远程过程调用。
(3)编写回调方法:
我们的回调模块必须导出六个回调方法: init/1 handle_call/3 handle_cast/2 handle_info/2 terminate/2 code_change/3 。为简单起见,可以使用一些模板来制作 gen_server 。下面是最简单的一种:
%% gen_server_template.mini
-module().
%% gen_server迷你模板
-behaviour(gen_server).
-export([start_link/0]).
%% gen_server回调
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

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

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

handle_call(_Request, _From, State) -> {reply, Reply, state}.

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

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

terminate(_Reason, state) -> ok.

code_change(OldVsn, State, Extra) -> {ok,State}.
这个模板包含了一套简单的框架,可以填充它们来制作服务器。如果忘记定义合适的回调函数,编译器就会根据关键字 -behaviour 来生成警告或错误消息。 start_link() 函数里的服务器名(宏?SERVER )需要进行定义,因为它默认是没有定义的。
我们将从模板入手,对它稍作修改。要做的就是让接口方法里的参数与模板里的参数保持一致。 handle_call/3函数最为重要。我们必须编写代码,让它匹配接口方法里定义的三种查询数据类型。也就是说,必须填写以下代码里的这些点:
handle_call({new, who}, From, State) ->
    Reply = ...,
    Statel = ...,
    {reply, Reply, Statel};
handle_call({add, Who, Amount}, From, State) ->
    Reply = ...,
    Statel = ...,
    {reply, Reply, Statel};
handle_call({remove, Who, Amount}, From, State) ->
    Reply = ...,
    Statel = ...,
    {reply, Reply, State1};
这段代码里的 Reply 值会作为远程过程调用的返回值发回客户端。State只是一个代表服务器全局状态的变量,它会在服务器里到处传递。在我们的银行模块里,这个状态永远不会发生变化,它只是一个ETS 表的索引,属于常量(虽然表的内容会变化)。填写模板并稍加改动之后,就形成了以下代码:
%% my_bank.erl
init([]) -> {ok, ets:new(?MODULE,[])}.

handle_call({new, Who}, From, Tab) ->
    Reply = case ets:lookup(Tab, Who) of
                [] -> ets:insert(Tab, {who, 0}),
                      {welcome, who};
                [_] -> {who, you_already_are_a_customer}
            end,
    {reply, Reply, Tab};
handle_call({add, Who, X}, From, Tab) ->
    Reply = case ets:lookup(Tab,Who) of
                [] -> not_a_customer;
                [{Who, Balance}] ->
                    NewBalance = Balance + X,
                    ets:insert(Tab, {Who, NewBalance}),
                    {thanks, Who, your_balance_is, NewBalance}
            end, 
    {reply, Reply, Tab};
handle_call({remove, Who, X}, _From, Tab) ->
    Reply = case ets:lookup(Tab, Who) of
                [] -> not_a_customer;
                [{Who, Balance}] when X = Balance ->
                    NewBalance = Balance - X,
                    ets:insert(Tab,{Who,NewBalance}),
                    {thanks, Who, your_balance_is, NewBalance};
                [{Who, Balance}] -> 
                    {sorry, Who,you_only_have, Balance, in_the_bank}
            end,
    {reply, Reply, Tab};
handle_call(stop, _From, Tab) ->
    {stop, normal, stopped, Tab}.

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

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

terminate(Reason, State) -> ok.

code_change(_oldVsn, State, _Extra) -> {ok, State}.
调用 gen_server:start_link(Name, CallBackMod,  StartArgs,  Opts) 来启动服务器,之后第一个被调用的回调模块方法是Mod:init(StartArgs) ,它必须返回 {ok, State} State的值作为handle_call的第三个参数重新出现。请注意我们是如何停止服务器的。 handle_call(stop,  From,  Tab)返回 {stop,  normal, stopped,  Tab},它会停止服务器。第二个参数(normal)被用作 my_bank:terminate/2的首个参数。第三个参数(stopped)会成为 my_bank:stop()的返回值。


就是这样,我们的任务完成了。下面来访问一下银行:

1> my_bank:start().
{0k, <0.33.0>}

2> my_bank:deposit("joe", 10).
not_a_customer

3> my_bank:new_account("joe").
{welcome, "joe"}

4> my_bank:deposit("joe", 10).
{thanks, "joe", your_balance_is, 10}

5> my_bank:deposit("joe", 30).
{thanks, "joe", your_balance_is, 40}

6> my_bank:withdraw("joe", 15).
{thanks, "joe", your_balance_is, 25}

7> my_bank:withdraw("joe", 45).
{sorry, "joe", you_only_have, 25, in_the_bank}

2.gen_server 的回调结构

(1)启动服务器:gen_server:start_link(Name, Mod, InitArgs, Opts)这个调用是所有事物的起点。它会创建一个名为Name的通用服务器,回调模块是Mod,Opts则控制通用服务器的行为。在这里可以指定消息记录、函数调试和其他行为。通用服务器通过调用Mod:init(InitArgs)启动。下面展示了一个init的模板项

%% -----------------------------------------------------
%% @private
%% @d0C
%% 品初始化服务器
%%
%% @spec init(Args) -> {ok, State} |
%%                     {ok, State, Timeout} |
%%                     ignore |
%%                     {stop,Reason}
%% @end
%% ----------------------------------------------------
init([]) ->
    {ok, #state{}}.

在通常的操作里,只会返回{ok, State}。如果返回{ok, State},就说明我们成功启动了服务器,它的初始状态是State

(2)调用服务器:要调用服务器,客户端程序需要执行gen_server:call(Name, Request)。它最终调用的是回调模块里的handle_call/3。handle_call/3的模板项如下:

%% -----------------------------------------------------------------
%% @private
%% @d0C
%% 处理调用消息
%%
%% @spec handle_call(Request, From, State) ->
%%                                {reply, Reply, State} |
%%                                {reply, Reply, State, Timeout} |
%%                                {noreply, State} |
%%                                {noreply, State, Timeout} |
%%                                {stop, Reason, Reply, State} |
%%                                {stop, Reason, State}
%% @end
%% ----------------------------------------------------------------
handle_call(_Request, _From, State) ->
    Reply = ok,
    {reply, Reply, State}.
Request gen_server:call/2 的第二个参数)作为 handle_call/3 的第一个参数重新出现。From是发送请求的客户端进程的 PID State则是客户端的当前状态。我们通常会返回 {reply, Reply, NewState}。在这种情况下,Reply会返回客户端,成为gen_server:call的返回值。NewState则是服务器接下来的状态。其他的返回值 ({noreply, ..}和{stop, ..})相对不太常用。no reply会让服务器继续工作,但客户端会等待一个回复,所以服务器必须把回复的任务委派给其他进程。用适当的参数调用stop会停止服务器。

(3) 调用和播发:我们已经见过了gen_server:callhandle_call之间的交互,它的作用是实现远程过程调用gen_server:cast(Name, Msg)则实现了一个播发(cast),也就是没有返回值的调用(实际上就是一个消息,但习惯上称它为播发来与远程过程调用相区分)。对应的回调方法是handle_cast,它的模板项如下:

%% ------------------------------------------------------------------
%% @private
%% @d0C
%% 处理播发消息
%%
%% @spec handle_cast(Msg,State) -> {noreply, State} |
%%                                 {noreply, State, Timeout} |
%%                                 {stop, Reason, State}
%% @end
%% -----------------------------------------------------------------
handle_cast(Msg, State) ->
    {noreply, State}.
这个处理函数通常只返回 {noreply, NewState} {stop, ...} 。前者改变服务器的状态,后者停止服务器。

(4) 发给服务器的自发性息:回调函数handle_info(Info, State)被用来处理发给服务器的自发性消息。自发性消息是一切未经显式调用gen_server:callgen_server:cast而到达服务器的消息。举个例子,如果服务器连接到另一个进程并捕捉退出信号,就可能会突然收到一个预料之外的{'EXIT', Pid, What}消息。除此之外,系统里任何知道通用服务器PID的进程都可以向它发送消息。这样的消息在服务器里表现为info值。handle_info的模板项如下:

%% ------------------------------------------------------------------
%% @private
%% @doc
%% 处理所有非调用/播发的消息
%%
%% @spec handle_info(Info, State) -> {noreply, State} |
%%                                   {noreply, State, Timeout} |
%%                                   {stop, Reason, State}
%% @end
%% ------------------------------------------------------------------
handle_info(Info, State) ->
{noreply, State}.
它的返回值和 handle_cast 相同。
(5) 服务器终止:服务器会因为许多原因而终止。某个以 handle_开头的函数也许会返回一个 {stop, Reason,NewState},服务器也可能崩溃并生成 {'EXIT', reason}。在所有这些情况下,无论它们是怎样发生的,都会调用 terminate(Reason, NewState)。它的模板项如下:
%% -------------------------------------------------------------------
%% @private
%% @doc
%% 这个函数是在某个gen_server即将终止时调用的。
%% 它应当是Module:init/1的逆操作,并进行必要的清理。
%% 当它返回时,<mod>gen_server</mod>终止并生成原因Reason。
%% 它的返回值会被忽略。
%%
%% @spec terminate(Reason, State) -> void()
%% @end
%% -------------------------------------------------------------------
terminate(_Reason, _State) ->
    ok.
这段代码不能返回一个新状态,因为我们已经终止了。但是了解服务器在终止时的状态非常有用。可以把状态保存到磁盘,把它放入消息发送给别的进程,或者根据应用程序的意愿丢弃它。如果想让服务器过后重启,就必须编写一个“我胡汉三又回来了”的函数,由terminate/2 触发。
(6)代码更改 :你可以在服务器运行时动态更改它的状态。这个回调函数会在系统执行软件升级时由版本处理子系统调用。如下所示:
%% ----------------------------------------------------------------
%% @private
%% @doc
%% 在代码更改时转换进程状态
%%
%% @spec code_change(oldVsn, State, Extra) -> {ok, NewState}
%% @end
%% ----------------------------------------------------------------
code_change(_OldVsn, State, Extra) ->
    {ok, State}.

3.填写 gen_server 模板

编写 OTP gen_server 大致上就是用你的代码填充一个预制模板,下面是一个例子。前面分别列出了gen_server的各个区块。我通过填充模板生成了一个名为my_bank的银行模块。这段代码取自模板。我移除了模板里的所有注释,这样就能清楚地看到代码的结构。如下所示:
%% my_bank.erl
-module(my_bank).

-behaviour(gen_server).
-export([start/0]).
%%gen_server回调函数
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-compile(export_all).
-define(SERVER, ?MODULE).

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

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

new_account(Who) -> gen_server:call(?MODULE, {new, Who}).

deposit(Who, Amount) -> gen_server:call(?MODULE, {add, Who, Amount}).

withdraw(Who, Amount) -> gen_server:call(?MODULE, {remove, Who, Amount}).

init([]) -> {ok, ets:new(?MODULE, [])}.

handle_call({new, Who}, From, Tab) ->
    Reply = case ets:lookup(Tab, Who) of
                    [] -> ets:insert(Tab, {Who,0}),
                          {welcome, Who};
                    [_] -> {who, you_already_are_a_customer}
            end,
    {reply, Reply, Tab};
handle_call({add, Who, X}, From, Tab) ->
    Reply = case ets:lookup(Tab, Who) of
                    [] -> not_a_customer;
                    [{Who, Balance}] ->
                        NewBalance = Balance + X,
                        ets:insert(Tab, {who, NewBalance}),
                        {thanks, Who, your_balance_is, NewBalance}
            end,
    {reply, Reply, Tab};
handle_call(remove, Who, X}, From, Tab) ->
    Reply = case ets:lookup(Tab,Who) of
                    [] -> not_a_customer;
                    [{Who,Balance}] when X =< Balance ->
                        NewBalance = Balance - X,
                        ets:insert(Tab, {Who, NewBalance}),
                        {thanks, Who, your_balance_is, NewBalance};
                    [{Who, Balance}] ->
                        {sorry, Who, you_only_have, Balance, in_the_bank}
            end,
    {reply, Reply, Tab};
handle_call(stop, _From, Tab) ->
    {stop, normal, stopped, Tab}.

handle_cast(_Msg, State) -> {noreply, State}.
handle_info(_Info, State) -> {noreply, State}.
terminate(_Reason, _State) -> ok.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
gen_server 的用途很广,但它并不能包治百病。 gen_server 的客户端 - 服务器交互模式有时候会让人感觉别扭,与你的问题不能良好兼容。如果是这样,就需要重新思考制作gen_server所需要的转变步骤,根据问题的特殊需要来修改它们。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

明明如皓

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值