如何在Elixir或Erlang中使用GenServer实现可重置的倒计时器

8
如何使用GenServer实现可重置的倒计时器?
1)在固定时间后执行任务,例如每60秒执行一次。
2)在计时器到期之前有一种方法可以将倒计时重置为60秒。
我已经查看了Erlang's gen_server如何定期执行操作?,但它并没有涵盖在倒计时结束之前重置计时器的方面。
谢谢。
2个回答

16

如何在Elixir或Phoenix框架中定时运行代码?中定义了如何执行周期性任务。

要基于此实现取消功能,可以按照以下步骤进行:

defmodule MyApp.Periodically do
  use GenServer

  def start_link(_name \\ nil) do
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  end

  def reset_timer() do
    GenServer.call(__MODULE__, :reset_timer)
  end

  def init(state) do
    timer = Process.send_after(self(), :work, 60_000)
    {:ok, %{timer: timer}}
  end

  def handle_call(:reset_timer, _from, %{timer: timer}) do
    :timer.cancel(timer)
    timer = Process.send_after(self(), :work, 60_000)
    {:reply, :ok, %{timer: timer}}
  end

  def handle_info(:work, state) do
    # Do the work you desire here

    # Start the timer again
    timer = Process.send_after(self(), :work,60_000)

    {:noreply, %{timer: timer}}
  end

  # So that unhanded messages don't error
  def handle_info(_, state) do
    {:ok, state}
  end
end

这个操作会保留对计时器的引用,以便可以取消它。每次接收到:work消息时,都会创建一个新的计时器并将其存储在GenServer的状态中。


4
Process.send_after(self(), :work, 60_000) 返回的计时器参考无法使用 :timer.cancel(timer) 取消,因为它会返回 {:error, :badarg}。但是,我可以使用 :erlang.cancel_timer(timer)。 我正在使用 Elixir v1.1.1 版本。 - Jason Harrelson
3
使用 Elixir 1.5+,可以通过 Process.cancel_timer(timer) 取消计时器。 - Mark Eric

4
如果您使用Erlang,如您所参考的其他问题所述...
您需要保存计时器引用,并调用erlang:cancel_timer/1来阻止它触发(如果尚未触发)。您必须注意这种“已经触发”的竞争条件,在您取消计时器时触发消息已经在您的消息队列中。您可能会关心这个问题,也可能不关心,但如果重要的是在触发被取消后永远不执行操作,则需要创建一个引用(或者您可以使用计数器)来设置定时消息,并且当您收到触发器时,您必须检查它是否与最新的触发器相关。
然后,其他问题中的代码变为:
-define(INTERVAL, 60000). % One minute

init(Args) ->
    ...
    % Start first timer
    MyRef = erlang:make_ref(),
    {ok, TRef} = erlang:send_after(?INTERVAL, self(), {trigger, MyRef}),
    ...
    {ok, State#your_record{
        timer  = TRef,
        latest = MyRef
        }}.

% Trigger only when the reference in the trigger message is the same as in State
handle_info({trigger, MyRef}, State = #your_record{latest=MyRef}) ->
    % Do the action
    ...
    % Start next timer
    MyRef = erlang:make_ref(),
    {ok, TRef} = erlang:send_after(?INTERVAL, self(), trigger),
    ...
    {ok, State#your_record{
        timer  = TRef,
        latest = MyRef
        }}.

% Ignore this trigger, it has been superceeded!
handle_info({trigger, _OldRef}, State) ->
    {ok, State}.

重置计时器的方法如下:

handle_info(reset, State = #your_record{timer=TRef}) ->
    % Cancel old timer
    erlang:cancel_timer(TRef),
    % Start next timer
    MyNewRef = erlang:make_ref(),
    {ok, NewTRef} = erlang:send_after(?INTERVAL, self(), trigger),
    {ok, State#your_record{
        timer  = NewTRef,
        latest = MyNewRef
        }}.

对于取消功能,调用可能更加合适,但这取决于您的应用,所以由您决定。

从技术上讲,取消计时器并不是必需的,因为一旦您使用新引用创建了新状态,即使旧计时器继续运行,当它触发时也会被忽略,但我认为最好还是进行清理工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接