事件处理程序

处理一下!*举起猎枪*

在前面的一些例子中,我一直在避免涉及某个特定的东西。如果你回顾一下提醒应用,你会发现我不知怎么地提到了我们可以通知客户端,无论它们是 IM、邮件等。在上一章中,我们的交易系统使用io:format/2来通知人们发生了什么。

你可能已经看到了这两种情况之间的共同点。它们都是关于让人们(或某个进程或应用程序)了解在某个时间点发生的事件。在一种情况下,我们只输出结果,而在另一种情况下,我们在向他们发送消息之前获取了订阅者的 Pid。

输出方法很简单,不容易扩展。使用订阅者的方法当然有效。事实上,当每个订阅者在收到事件后都有一个长时间运行的操作时,它非常有用。在更简单的情况下,如果你不一定要为每个回调创建一个等待事件的备用进程,就可以采用第三种方法。

第三种方法只采用一个接受函数的进程,并让它们在任何传入的事件上运行。这个进程通常被称为事件管理器,它可能最终看起来像这样

Shows a bubble labeled 'your server', another one labeled 'event manager'. An arrow (representing an event) goes from your server to the event manager, which has the event running in callback functions (illustrated as f, y and g)

这样做的方式有几个优点

当然,也有一些缺点

有一个方法可以解决这些缺点,不过有点平淡无奇。基本上,你必须将事件管理器方法变成订阅者方法。幸运的是,事件管理器方法足够灵活,可以轻松地做到这一点,我们将在本章后面看到如何做到这一点。

我通常会在纯 Erlang 中先编写 OTP 行为的非常基本版本,但在这种情况下,我将直接切入主题。这里就是gen_event

通用事件处理程序

gen_event 行为与gen_servergen_fsm 行为有很大不同,因为你永远不会真正启动一个进程。我上面描述的关于“接受回调”的整个部分就是原因所在。gen_event 行为基本上运行接受并调用函数的进程,你只需要提供一个包含这些函数的模块。也就是说,除了以事件管理器喜欢的格式提供回调函数外,你无需处理事件操作。所有管理都是免费完成的;你只需要提供与你的应用程序相关的部分。考虑到 OTP 一直在做的事情,这并不令人惊讶,它一直都在将通用部分与特定部分分离。

然而,这种分离意味着标准spawn -> init -> loop -> terminate 模式只适用于事件处理程序。现在,如果你回忆一下之前说过的内容,事件处理程序是在管理器中运行的一组函数。这意味着当前提出的模型

common process pattern: spawn -> init -> loop -> exit

切换到对程序员来说更类似于这样的东西

 spawn event manager -> attach handler -> init handler -> loop (with handler calls) -> exit handlers

每个事件处理程序都可以保存自己的状态,由管理器为它们携带。然后,每个事件处理程序都可以采用以下形式

init --> handle events + special messages --> terminate

这并不复杂,所以让我们继续关注事件处理程序的回调本身。

init 和 terminate

init 和 terminate 函数类似于我们在之前关于服务器和有限状态机的行为中看到的。init/1 接受一个参数列表,并返回{ok, State}。在init/1 中发生的事情应该在terminate/2 中有对应部分。

handle_event

handle_event(Event, State) 函数基本上是gen_event 回调模块的核心。它的工作方式类似于gen_serverhandle_cast/2,它异步工作。不过,它在返回值方面有所不同

A sleeping polar bear with a birthday hat

元组{ok, NewState} 的工作方式类似于我们在gen_server:handle_cast/2 中看到的;它只是更新自己的状态,不会回复任何人。在{ok, NewState, hibernate} 的情况下,需要注意的是,整个事件管理器将被置于休眠状态。请记住,事件处理程序在与它们的管理器相同的进程中运行。然后,remove_handler 将处理程序从管理器中删除。这在你自己的事件处理程序知道它已经完成,没有其他事情需要做时非常有用。最后,还有{swap_handler, Args1, NewState, NewHandler, Args2}。这个函数并不经常使用,但它的作用是删除当前的事件处理程序,并用一个新的事件处理程序替换它。这是通过首先调用CurrentHandler:terminate(Args1, NewState) 并删除当前的处理程序,然后通过调用NewHandler:init(Args2, ResultFromTerminate) 添加一个新的处理程序来完成的。这在你了解某个特定事件已发生,并且最好将控制权交给一个新的处理程序时非常有用。这很可能是在你了解何时需要它的时候做的事情。再说一次,它并不经常使用。

所有传入的事件都可以来自gen_event:notify/2,它像gen_server:cast/2 一样是异步的。还有一个gen_event:sync_notify/2,它是同步的。说起来有点奇怪,因为handle_event/2 仍然是异步的。这里的想法是,函数调用只在所有事件处理程序都看到并处理了新消息后才会返回。在此之前,事件管理器将通过不回复来一直阻塞调用进程。

handle_call

这类似于gen_serverhandle_call 回调,只是它可以返回{ok, Reply, NewState}{ok, Reply, NewState, hibernate}{remove_handler, Reply}{swap_handler, Reply, Args1, NewState, Handler2, Args2}gen_event:call/3-4 函数用于进行调用。

这就产生了一个问题。当我们有 15 个不同的事件处理程序时,这将如何运作?我们是否期望得到 15 个回复,或者只期望得到一个包含所有回复的回复?实际上,我们将被迫选择一个处理程序来回复我们。当我们实际看到如何将处理程序附加到我们的事件管理器时,我们将深入了解这将如何实现,但如果你不耐烦,你可以查看函数gen_event:add_handler/3 的工作方式,尝试弄清楚。

handle_info

handle_info/2 回调与handle_event 几乎相同(相同的返回值以及所有内容),区别在于它只处理带外消息,例如退出信号、使用! 运算符直接发送到事件管理器的消息等。它的用例类似于gen_servergen_fsm 中的handle_info 的用例。

code_change

代码更改的工作方式与gen_server 的工作方式完全相同,只是它针对每个单独的事件处理程序。它接受 3 个参数,OldVsnStateExtra,它们分别是版本号、当前处理程序的状态和我们可以暂时忽略的数据。它只需要返回{ok, NewState} 即可。

是时候玩冰壶了!

通过查看回调,我们可以开始研究使用gen_event 实现一些东西。在本章的这一部分中,我选择创建一组用于跟踪世界上最有趣的运动之一冰壶的比赛更新的事件处理程序。

如果你从未见过或玩过冰壶(这太可惜了!),规则相对简单

A top view of a curling ice/game

你有两支队伍,它们试图让一块冰壶石头 在冰面上滑动,并试图到达红色圆圈的中心。它们用 16 块石头来完成这个动作,在每轮(称为回合)结束时,石头最靠近中心的队伍获胜一分。如果一支队伍拥有最靠近中心的两个石头,它将获得两分,以此类推。共有 10 个回合,在 10 个回合结束后得分最高的队伍获胜。

还有更多规则使比赛更加迷人,但这是一本关于 Erlang 的书,而不是关于极度迷人的冬季运动的书。如果你想了解更多关于规则的知识,我建议你前往维基百科关于冰壶的条目

对于这个完全与现实世界相关的场景,我们将为下一届冬季奥运会工作。发生一切的城市刚刚完成了比赛场馆的建造,他们正在努力准备记分牌。事实证明,我们必须编写一个系统,让一些官员输入比赛事件,例如石头被扔出去时、回合结束时或比赛结束时,然后将这些事件路由到记分牌、统计系统、新闻记者的订阅源等。

我们非常聪明,知道这是关于 gen_event 的一章,并推断出我们很可能会使用它来完成我们的任务。考虑到这只是一个例子,我们不会实现所有规则,但请随时在本章结束时这样做。我保证不会生气。

我们将从记分牌开始。由于他们现在正在安装它,我们将使用一个假的模块,该模块通常允许我们与它交互,但现在它只会使用标准输出来显示正在发生的事情。这就是curling_scoreboard_hw.erl 的作用

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

%% This is a 'dumb' module that's only there to replace what a real hardware
%% controller would likely do. The real hardware controller would likely hold
%% some state and make sure everything works right, but this one doesn't mind.

%% Shows the teams on the scoreboard.
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").

所以这就是记分牌的所有功能。它们通常有一个计时器和其他很棒的功能,但不管怎样。看起来奥委会并不想让我们为教程实现一些琐碎的事情。

这个硬件接口让我们有一些设计时间。我们知道目前有一些事件需要处理:添加队伍、进入下一回合、设置积分。我们只会在开始新比赛时使用reset_board 功能,而不需要把它作为我们协议的一部分。我们需要处理的事件可能在我们的协议中采用以下形式

我们可以从这个基本的事件处理程序框架开始我们的实现。

-module(curling_scoreboard).
-behaviour(gen_event).

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

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

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

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

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

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

terminate(_Reason, _State) ->
    ok.

这是一个框架,我们可以用它来处理所有的gen_event回调模块。目前,记分牌事件处理程序本身不需要做任何特殊的事情,只需要将调用转发到硬件模块即可。我们希望事件来自gen_event:notify/2,因此协议的处理应该在handle_event/2中完成。文件curling_scoreboard.erl显示了更新。

-module(curling_scoreboard).
-behaviour(gen_event).

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

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}.

你可以看到对handle_event/2函数所做的更新。试试看。

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.43.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

这里发生了一些事情。首先,我们将gen_event进程作为一个独立的进程启动。然后,我们使用gen_event:add_handler/3动态地将我们的事件处理程序附加到它。你可以根据需要进行多次操作。然而,正如之前在handle_call部分提到的,当你想要使用特定的事件处理程序时,这可能会造成问题。如果你想要在有多个事件处理程序实例时调用、添加或删除特定的处理程序,你必须找到一种方法来唯一地标识它。我最喜欢的方法(如果你没有更具体的考虑,这个方法很有效)就是使用make_ref()作为唯一的值。为了将这个值提供给处理程序,你可以通过调用add_handler/3作为gen_event:add_handler(Pid, {Module, Ref}, Args)来添加它。从现在开始,你可以使用{Module, Ref}与该特定处理程序进行通信。问题解决了。

A curling stone

无论如何,你可以看到我们向事件处理程序发送消息,它成功地调用了硬件模块。然后我们删除了处理程序。这里,turn_offterminate/2函数的参数,我们的实现目前并不关心它。处理程序消失了,但我们仍然可以向事件管理器发送事件。万岁。

上面的代码片段有一个奇怪的地方,那就是我们被迫直接调用gen_event模块,并向所有人展示我们的协议是什么样子。更好的选择是在它的上面提供一个抽象模块,它只封装了我们需要的全部内容。这将对使用我们代码的每个人来说更加友好,并且让我们能够在需要时(或当需要时)更改实现。它还让我们能够指定为标准的冰壶比赛必须包含哪些处理程序。

-module(curling).
-export([start_link/2, set_teams/3, add_points/3, next_round/1]).

start_link(TeamA, TeamB) ->
    {ok, Pid} = gen_event:start_link(),
    %% The scoreboard will always be there
    gen_event:add_handler(Pid, curling_scoreboard, []),
    set_teams(Pid, TeamA, TeamB),
    {ok, Pid}.

set_teams(Pid, TeamA, TeamB) ->
    gen_event:notify(Pid, {set_teams, TeamA, TeamB}).

add_points(Pid, Team, N) ->
    gen_event:notify(Pid, {add_points, Team, N}).

next_round(Pid) ->
    gen_event:notify(Pid, next_round).

现在运行它。

1> c(curling).
{ok,curling}
2> {ok, Pid} = curling:start_link("Pirates", "Scotsmen").
Scoreboard: Team Pirates vs. Team Scotsmen
{ok,<0.78.0>}
3> curling:add_points(Pid, "Scotsmen", 2). 
Scoreboard: increased score of team Scotsmen by 1
Scoreboard: increased score of team Scotsmen by 1
ok
4> curling:next_round(Pid). 
Scoreboard: round over
ok
Some kind of weird looking alien sitting on a toilet, surprised at the newspapers it is reading

这看起来并没有多大优势,但它实际上是关于使代码更易于使用(并减少了写错消息的可能性)。

向媒体发出警报!

我们已经完成了基本的记分牌,现在我们希望国际记者能够从我们负责更新系统的人员那里获得实时数据。因为这是一个示例程序,我们不会介绍设置套接字和编写更新协议的步骤,但我们会通过放置一个中介进程来负责实现这个系统。

基本上,每当一家新闻机构想要进入比赛信息流时,他们都会注册自己的处理程序,该处理程序只将他们需要的数据转发给他们。我们将有效地将我们的gen_event服务器变成某种消息中心,只是将它们路由到需要它们的人。

首先要做的就是更新curling.erl模块,使其具有新的接口。因为我们希望事情易于使用,我们只添加两个函数,join_feed/2leave_feed/2。加入信息流应该可以通过输入事件管理器的正确Pid和将所有事件转发到的Pid来完成。这应该返回一个唯一的值,然后可以使用该值通过leave_feed/2取消订阅信息流。

%% Subscribes the pid ToPid to the event feed.
%% The specific event handler for the newsfeed is
%% returned in case someone wants to leave
join_feed(Pid, ToPid) ->
    HandlerId = {curling_feed, make_ref()},
    gen_event:add_handler(Pid, HandlerId, [ToPid]),
    HandlerId.

leave_feed(Pid, HandlerId) ->
    gen_event:delete_handler(Pid, HandlerId, leave_feed).

请注意,我正在使用之前描述的用于多个处理程序的技术({curling_feed, make_ref()})。你可以看到,这个函数期望一个名为curling_feed的gen_event回调模块。如果我只使用模块名作为HandlerId,事情仍然可以正常工作,只是我们无法控制在完成一个实例后删除哪个处理程序。事件管理器只会以未定义的方式选择其中一个。使用Ref可以确保来自Head-Smashed-In Buffalo Jump的记者离开现场不会断开来自The Economist的记者的连接(我不知道为什么他们会做关于冰壶的报道,但谁知道呢)。无论如何,这是我为curling_feed模块实现的代码。

-module(curling_feed).
-behaviour(gen_event).

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

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

handle_event(Event, Pid) ->
    Pid ! {curling_feed, Event},
    {ok, Pid}.

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

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

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

terminate(_Reason, _State) ->
    ok.

这里唯一有趣的事情仍然是handle_event/2函数,它会盲目地将所有事件转发到订阅的Pid。现在,当我们使用新模块时。

1> c(curling), c(curling_feed).
{ok,curling_feed}
2> {ok, Pid} = curling:start_link("Saskatchewan Roughriders", "Ottawa Roughriders").
Scoreboard: Team Saskatchewan Roughriders vs. Team Ottawa Roughriders
{ok,<0.165.0>}
3> HandlerId = curling:join_feed(Pid, self()). 
{curling_feed,#Ref<0.0.0.909>}
4> curling:add_points(Pid, "Saskatchewan Roughriders", 2). 
Scoreboard: increased score of team Saskatchewan Roughriders by 1
ok
Scoreboard: increased score of team Saskatchewan Roughriders by 1
5> flush().
Shell got {curling_feed,{add_points,"Saskatchewan Roughriders",2}}
ok
6> curling:leave_feed(Pid, HandlerId).
ok
7> curling:next_round(Pid). 
Scoreboard: round over
ok
8> flush().
ok

我们可以看到,我们自己加入了信息流,收到了更新,然后离开了,不再接收更新。你实际上可以尝试多次添加多个进程,它将正常工作。

但这带来了一个问题。如果冰壶信息流的订阅者之一崩溃了怎么办?我们是否只是让处理程序继续在那里运行?理想情况下,我们不必这样做。事实上,我们不必这样做。只需要将gen_event:add_handler/3中的调用更改为gen_event:add_sup_handler/3即可。如果崩溃了,处理程序就会消失。然后在另一端,如果gen_event管理器崩溃了,消息{gen_event_EXIT, Handler, Reason}会发送给你,这样你就可以处理它。很容易吧?再想想。

不要喝太多酷乐饮料。

alien kid on a leash

在你童年的时候,你可能去过你阿姨或祖母家参加聚会或其他活动。如果你很调皮,除了你的父母之外,还会有几个大人在看着你。如果你做错了什么,你的妈妈、爸爸、阿姨、祖母都会责骂你,然后即使你已经清楚地知道自己做错了,他们也会一直告诉你。好吧,gen_event:add_sup_handler/3有点像这样;不,说真的。

每当使用gen_event:add_sup_handler/3时,就会在你的进程和事件管理器之间建立一个链接,以便它们都被监督,并且处理程序知道它的父进程是否失败。如果你还记得错误和进程一章以及关于监视器的部分,我提到过监视器非常适合编写需要了解其他进程状况的库,因为它们可以堆叠,这与链接相反。好吧,gen_event早于Erlang中监视器的出现,对向后兼容性的坚定承诺引入了这个非常糟糕的弊端。基本上,因为同一个进程可以作为许多事件处理程序的父进程,所以库永远不会取消链接这些进程(除非它永久终止),以防万一。监视器实际上可以解决这个问题,但它们没有被用于那里。

这意味着当你的进程崩溃时,一切都会正常:被监督的处理程序会被终止(通过调用YourModule:terminate({stop, Reason}, State))。当处理程序本身崩溃(但事件管理器没有崩溃)时,一切都会正常:你将收到{gen_event_EXIT, HandlerId, Reason}。但是,当事件管理器关闭时,你将:

这是一个相当大的弊端,但至少你知道了。如果你愿意,可以尝试将你的事件处理程序切换到一个受监督的处理程序。即使它在某些情况下有可能会更烦人,它也更安全。安全第一。

我们还没有完成!如果媒体的某位成员没有及时到达会怎么样?我们需要能够从信息流中告诉他们比赛的当前状态。为此,我们将编写一个名为curling_accumulator的额外事件处理程序。同样,在完全编写它之前,我们可能希望通过几个想要的调用将其添加到curling模块中。

-module(curling).
-export([start_link/2, set_teams/3, add_points/3, next_round/1]).
-export([join_feed/2, leave_feed/2]).
-export([game_info/1]).

start_link(TeamA, TeamB) ->
    {ok, Pid} = gen_event:start_link(),
    %% The scoreboard will always be there
    gen_event:add_handler(Pid, curling_scoreboard, []),
    %% Start the stats accumulator
    gen_event:add_handler(Pid, curling_accumulator, []),
    set_teams(Pid, TeamA, TeamB),
    {ok, Pid}.

%% skipping code here

%% Returns the current game state.
game_info(Pid) ->
    gen_event:call(Pid, curling_accumulator, game_data).

需要注意的是,game_info/1函数只使用curling_accumulator作为处理程序id。如果你有许多相同处理程序的版本,关于使用make_ref()(或任何其他方法)来确保你写入正确处理程序的提示仍然适用。还要注意,我让curling_accumulator处理程序自动启动,就像记分牌一样。现在来看模块本身。它应该能够保存冰壶比赛的状态:到目前为止,我们有队伍、得分和轮次需要跟踪。所有这些都可以保存在一个状态记录中,并在收到每个事件时进行更改。然后,我们只需要回复game_data调用,如下所示。

-module(curling_accumulator).
-behaviour(gen_event).

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

-record(state, {teams=orddict:new(), round=0}).

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

handle_event({set_teams, TeamA, TeamB}, S=#state{teams=T}) ->
    Teams = orddict:store(TeamA, 0, orddict:store(TeamB, 0, T)),
    {ok, S#state{teams=Teams}};
handle_event({add_points, Team, N}, S=#state{teams=T}) ->
    Teams = orddict:update_counter(Team, N, T),
    {ok, S#state{teams=Teams}};
handle_event(next_round, S=#state{}) ->
    {ok, S#state{round = S#state.round+1}};
handle_event(_Event, Pid) ->
    {ok, Pid}.

handle_call(game_data, S=#state{teams=T, round=R}) ->
    {ok, {orddict:to_list(T), {round, R}}, S};
handle_call(_, State) ->
    {ok, ok, State}.

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

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

terminate(_Reason, _State) ->
    ok.

所以你可以看到,我们基本上只是更新状态,直到有人询问比赛细节,这时我们会将它们发回给他们。我们用一种非常基本的方式做到了这一点。也许一种更智能的代码组织方式是简单地保留比赛中所有发生事件的列表,这样我们每次有新的进程订阅信息流时,都可以将它们一次性发回。这里不需要展示如何运作,所以让我们专注于使用我们的新代码。

1> c(curling), c(curling_accumulator).
{ok,curling_accumulator}
2> {ok, Pid} = curling:start_link("Pigeons", "Eagles").
Scoreboard: Team Pigeons vs. Team Eagles
{ok,<0.242.0>}
3> curling:add_points(Pid, "Pigeons", 2).
Scoreboard: increased score of team Pigeons by 1
ok
Scoreboard: increased score of team Pigeons by 1
4> curling:next_round(Pid).
Scoreboard: round over
ok
5> curling:add_points(Pid, "Eagles", 3).
Scoreboard: increased score of team Eagles by 1
ok
Scoreboard: increased score of team Eagles by 1
Scoreboard: increased score of team Eagles by 1
6> curling:next_round(Pid).
Scoreboard: round over
ok
7> curling:game_info(Pid).
{[{"Eagles",3},{"Pigeons",2}],{round,2}}

令人激动!奥运会组委会一定会喜欢我们的代码。我们可以拍拍自己的胸脯,兑现一张大额支票,然后彻夜玩电子游戏。

我们还没有看到关于gen_event作为一个模块的所有内容。事实上,我们还没有看到事件处理程序最常见的用途:日志记录和系统警报。我决定不展示它们,因为几乎所有其他关于Erlang的资料都严格地将gen_event用于此。如果你有兴趣了解它们,请先查看error_logger

即使我们没有看到gen_event最常见的用途,但重要的是要说我们已经看到了理解它们、构建自己的gen_event并将它们集成到我们的应用程序中所必需的所有概念。更重要的是,我们终于涵盖了主动代码开发中使用的三种主要OTP行为。我们还有几个行为需要访问——那些充当所有工作进程之间粘合剂的行为——比如监督器。